diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d6f67b63 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +#on: [push] + +# 在master分支发生push事件时触发。 +on: + push: + branches: + - master + +jobs: # 工作流 + build: # 自定义名称 + runs-on: ubuntu-latest #运行在虚拟机环境ubuntu-latest + + strategy: + matrix: + node-version: [14.x] + + steps: # 步骤 + - name: Checkout # 步骤1 + # 使用的动作。格式:userName/repoName。作用:检出仓库,获取源码。 官方actions库:https://github.com/actions + uses: actions/checkout@v1 + + - name: Use Node.js ${{ matrix.node-version }} # 步骤2 + uses: actions/setup-node@v1 # 作用:安装nodejs + with: + node-version: ${{ matrix.node-version }} # 版本 +#------------------------------------------------------------------- + - name: Configure Private Key #★★★★★★步骤3:设置ssh + env: + SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }} + run: | + mkdir -p ~/.ssh + echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + echo "StrictHostKeyChecking no" >> ~/.ssh/config + - name: Push Gitee Mirror #★★★★★★步骤4:推送到gitee + env: + SOURCE_REPO: 'https://github.com/oddfar/docs.git' + DESTINATION_REPO: 'git@gitee.com:oddfar/docs.git' + run: | + git clone --mirror "$SOURCE_REPO" && cd `basename "$SOURCE_REPO"` + git remote set-url --push origin "$DESTINATION_REPO" + git fetch -p origin + git for-each-ref --format 'delete %(refname)' refs/pull | git update-ref --stdin + git push --mirror +#------------------------------------------------------------------- + - name: run deploy.sh # 步骤5:执行脚本deploy.sh + env: # 设置环境变量,未设置则不运行 + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # toKen私密变量 + SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }} # gitee的ssh + + run: npm install && npm run deploy + + ## ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ + ## 注意:不需要同步到gitee镜像,则把步骤3和4删掉 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ec9529f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# npm +package-lock.json +node_modules + +# vscode +.vscode + +# vuepress +docs/.vuepress/dist + +# 百度链接推送 +urls.txt diff --git a/README.md b/README.md new file mode 100644 index 00000000..88be6e2b --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [准备](#%E5%87%86%E5%A4%87) +- [写作](#%E5%86%99%E4%BD%9C) +- [部署](#%E9%83%A8%E7%BD%B2) + - [手动部署到github](#%E6%89%8B%E5%8A%A8%E9%83%A8%E7%BD%B2%E5%88%B0github) + - [自动部署到github](#%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2%E5%88%B0github) + - [部署到自己服务器](#%E9%83%A8%E7%BD%B2%E5%88%B0%E8%87%AA%E5%B7%B1%E6%9C%8D%E5%8A%A1%E5%99%A8) + + + +使用[vuepress](https://vuepress.vuejs.org/zh)搭建,自动部署在[GitHub Pages](https://pages.github.com/) + +使用[vdoing](https://github.com/xugaoyi/vuepress-theme-vdoing)主题 + +## 准备 + +VuePress 需要 [Node.js ](https://nodejs.org/en/)>= 8.6 + +1. 克隆到本地并进入目录 + + ```sh + git clone https://github.com/oddfar/docs.git && cd docs + ``` + +2. 安装本地依赖 + + ```sh + npm install + ``` + +3. 本地测试 + + ```sh + npm run dev + ``` + + 默认访问链接:http://localhost:8080/doc + +## 写作 + +使用`markdown`语法编写`md`文件,所有笔记`md`文件放在`docs/docs`目录下 + +例如添加`test`类,并编写`hello.md`文件 + +1. 创建目录 + + 格式:序号+标题 + + 例如:30.test + +2. 添加笔记 + + 例如:01.hello.md + +3. 编写内容 + + ```markdown + --- + title: 笔记标题 + permalink: /test/hello/ + date: 2021-01-01 01:01:01 + --- + + ## 标题 + + hello world + ``` + + tittle:标题,不填写则默认文件名中的标题,即`hello` + + permalink:访问链接,不填写则自动生成 + + date:日期,默认文件创建时间 + +4. 测试运行 + + 在项目根目录下 + + ```sh + npm run dev + ``` + +详情请看[vdoing主题介绍文档](https://doc.xugaoyi.com/) + +## 部署 + +### 手动部署到github + +创建分支:`gh-pages` + +更改文件`deploy.sh`内容 + +```sh +githubUrl=git@github.com:oddfar/docs.git +``` + + + +```sh +githubUrl=https://oddfar:${GITHUB_TOKEN}@github.com/oddfar/docs.git +``` + +双击运行`deploy.sh` + +之后配置 [GitHub Pages](https://pages.github.com/) + +![image-20210517151354287](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210517151356.png) + +### 自动部署到github + +详情查看:https://github.com/oddfar/docs/blob/master/docs/10.%E5%85%B3%E4%BA%8E/02.%E5%85%B3%E4%BA%8E%20-%20%E6%9C%AC%E7%AB%99/05.%E6%96%87%E6%A1%A3%E7%9A%84%E9%83%A8%E7%BD%B2.md + +### 部署到自己服务器 + +根目录下执行命令 + +```sh +npm run build +``` + +生成文件在`docs\.vuepress\dist\`目录下 + +打包到服务器即可 + +注:本地不可直接访问,需要配合插件 \ No newline at end of file diff --git a/base.js b/base.js new file mode 100644 index 00000000..430c4a30 --- /dev/null +++ b/base.js @@ -0,0 +1 @@ +module.exports = '/' \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 00000000..f9e65d0f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env sh + +# 确保脚本抛出遇到的错误 +set -e + +initDist(){ + echo $1 > base.js +} + + + +#------------------------------------------ + +#url访问目录,这个是你 github 仓库的名字 +initDist "module.exports = '/docs/'" + +# 生成静态文件 +npm run build +# 进入生成的文件夹 +cd docs/.vuepress/dist + +# deploy to github +if [ -z "$GITHUB_TOKEN" ]; then + # 手动部署 + msg='deploy' + githubUrl=git@github.com:oddfar/docs.git +else + # 自动部署 + msg='来自github actions的自动部署' + githubUrl=https://oddfar:${GITHUB_TOKEN}@github.com/oddfar/docs.git + git config --global user.name "oddfar" + git config --global user.email "oddfar@163.com" +fi +git init +git add -A +git commit -m "${msg}" +git push -f $githubUrl master:gh-pages # 推送到github + + +cd - # 退回开始所在目录 +rm -rf docs/.vuepress/dist + +#------------------------------------------ + + +#打包代码同步到 gitee gh-pages分支 +if [ -z "$SSH_PRIVATE_KEY" ]; then + echo '如果是空字符串,则不部署到gitee' +else + #url访问目录 + initDist "module.exports = '/'" + # 生成静态文件 + npm run build + # 进入生成的文件夹 + cd docs/.vuepress/dist + + giteeUrl=git@gitee.com:oddfar/docs.git #gitee 仓库ssh地址 + + git config --global user.name "oddfar" + git config --global user.email "oddfar@163.com" + git init + git add -A + git commit -m "来自github actions的自动部署" + git push -f $giteeUrl master:gh-pages + + cd - # 退回开始所在目录 + rm -rf docs/.vuepress/dist + # 删除秘钥 + rm -rf ~/.ssh +fi + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js new file mode 100644 index 00000000..0a74fc66 --- /dev/null +++ b/docs/.vuepress/config.js @@ -0,0 +1,144 @@ +const nav = require('./config/nav.js'); +const base = require('../../base.js'); +const htmlModules = require('./config/htmlModules.js'); + +module.exports = { + title: "OddFar's Docs", + description: '我们在黑暗中并肩前行,走在各自的朝圣路上!', // 描述,以 标签渲染到页面html中 + base, //后缀 '/docs/' + head: [ + ['link', { rel: 'icon', href: '/img/favicon.ico' }], + ['meta', { name: 'keywords', content: 'oddfar,zhiyuan'}], + ['meta', { name: 'theme-color', content: '#11a8cd'}], // 移动浏览器主题颜色 + ], + markdown: { + lineNumbers: true // 代码行号 + }, + + theme: 'vdoing', // 使用依赖包主题 + + themeConfig: { // 主题配置 + nav, + sidebarDepth: 2, // 侧边栏显示深度,默认1,最大2(显示到h3标题) + logo: '/img/logo.png', // 导航栏logo + repo: 'oddfar/docs', // 导航栏右侧生成Github链接 + searchMaxSuggestions: 10, // 搜索结果显示最大数 + lastUpdated: '最后更新', // 更新的时间,及前缀文字 string | boolean (取值为git提交时间) + docsDir: 'docs', // 编辑的文件夹 + editLinks: true, // 启用编辑 + editLinkText: '在 GitHub 上编辑此页', + + // 以下配置是Vdoing主题改动的和新增的配置 + category: false, // 是否打开分类功能,默认true。 如打开,会做的事情有:1. 自动生成的frontmatter包含分类字段 2.页面中显示与分类相关的信息和模块 3.自动生成分类页面(在@pages文件夹)。如关闭,则反之。 + tag: false, // 是否打开标签功能,默认true。 如打开,会做的事情有:1. 自动生成的frontmatter包含标签字段 2.页面中显示与标签相关的信息和模块 3.自动生成标签页面(在@pages文件夹)。如关闭,则反之。 + // archive: false, // 是否打开归档功能,默认true。 如打开,会做的事情有:1.自动生成归档页面(在@pages文件夹)。如关闭,则反之。 + // categoryText: '随笔', // 碎片化文章(_posts文件夹的文章)预设生成的分类值,默认'随笔' + // bodyBgImg: [ + // 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200507175828.jpeg', + // 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200507175845.jpeg', + // 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200507175846.jpeg' + // ], // body背景大图,默认无。 单张图片 String || 多张图片 Array, 多张图片时每隔15秒换一张。 + // titleBadge: false, // 文章标题前的图标是否显示,默认true + // titleBadgeIcons: [ // 文章标题前图标的地址,默认主题内置图标 + // '图标地址1', + // '图标地址2' + // ], + + + + sidebar: 'structuring', // 侧边栏 'structuring' | { mode: 'structuring', collapsable: Boolean} | 'auto' | 自定义 温馨提示:目录页数据依赖于结构化的侧边栏数据,如果你不设置为'structuring',将无法使用目录页 + + // sidebarOpen: false, // 初始状态是否打开侧边栏,默认true + updateBar: { // 最近更新栏 + showToArticle: true, // 显示到文章页底部,默认true + // moreArticle: '/archives' // “更多文章”跳转的页面,默认'/archives' + }, + + author: { // 文章默认的作者信息,可在md文件中单独配置此信息 String | {name: String, href: String} + name: 'zhiyuan', // 必需 + href: 'https://oddfar.com/' // 可选的 + }, + // blogger:{ // 博主信息,显示在首页侧边栏 + // avatar: 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200103123203.jpg', + // name: 'Evan Xu', + // slogan: '前端界的小学生' + // }, + // social:{ // 社交图标,显示于博主信息栏和页脚栏 + // // iconfontCssFile: '//at.alicdn.com/t/font_1678482_u4nrnp8xp6g.css', // 可选,阿里图标库在线css文件地址,对于主题没有的图标可自由添加 + // icons: [ + // { + // iconClass: 'icon-youjian', + // title: '发邮件', + // link: 'mailto:894072666@qq.com' + // }, + // { + // iconClass: 'icon-github', + // title: 'GitHub', + // link: 'https://github.com/xugaoyi' + // }, + // { + // iconClass: 'icon-erji', + // title: '听音乐', + // link: 'https://music.163.com/#/playlist?id=755597173' + // } + // ] + // }, + footer:{ // 页脚信息 + createYear: 2021, // 博客创建年份 + copyrightInfo: 'oddfar | docs', // 博客版权信息,支持a标签 + } + }, + plugins: [ // 插件 + ['thirdparty-search', { // 可以添加第三方搜索链接的搜索框(原官方搜索框的参数仍可用) + thirdparty: [ // 可选,默认 [] + { + title: '在MDN中搜索', + frontUrl: 'https://developer.mozilla.org/zh-CN/search?q=', // 搜索链接的前面部分 + behindUrl: '' // 搜索链接的后面部分,可选,默认 '' + }, + { + title: '在Bing中搜索', + frontUrl: 'https://cn.bing.com/search?q=' + } + ] + }], + + // 'vuepress-plugin-baidu-autopush', // 百度自动推送 + + ['one-click-copy', { // 代码块复制按钮 + copySelector: ['div[class*="language-"] pre', 'div[class*="aside-code"] aside'], // String or Array + copyMessage: '复制成功', // default is 'Copy successfully and then paste it for use.' + duration: 1000, // prompt message display time. + showInMobile: false // whether to display on the mobile side, default: false. + }], + ['demo-block', { // demo演示模块 https://github.com/xiguaxigua/vuepress-plugin-demo-block + settings: { + // jsLib: ['http://xxx'], // 在线示例(jsfiddle, codepen)中的js依赖 + // cssLib: ['http://xxx'], // 在线示例中的css依赖 + // vue: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js', // 在线示例中的vue依赖 + jsfiddle: false, // 是否显示 jsfiddle 链接 + codepen: true, // 是否显示 codepen 链接 + horizontal: false // 是否展示为横向样式 + } + }], + [ + 'vuepress-plugin-zooming', // 放大图片 + { + selector:'.theme-vdoing-content img:not(.no-zoom)', // 排除class是no-zoom的图片 + options: { + bgColor: 'rgba(0,0,0,0.6)' + }, + }, + ], + + [ + '@vuepress/last-updated', // "上次更新"时间格式 + { + transformer: (timestamp, lang) => { + const moment = require('moment') // https://momentjs.com/ + return moment(timestamp).format('YYYY/MM/DD, H:MM:SS'); + } + } + ] + ] +} diff --git a/docs/.vuepress/config/htmlModules.js b/docs/.vuepress/config/htmlModules.js new file mode 100644 index 00000000..8518bb55 --- /dev/null +++ b/docs/.vuepress/config/htmlModules.js @@ -0,0 +1,105 @@ +/** 插入自定义html模块 (可用于插入广告模块等) + * { + * homeSidebarB: htmlString, 首页侧边栏底部 + * + * sidebarT: htmlString, 全局左侧边栏顶部 + * sidebarB: htmlString, 全局左侧边栏底部 + * + * pageT: htmlString, 全局页面顶部 + * pageB: htmlString, 全局页面底部 + * pageTshowMode: string, 页面顶部-显示方式:未配置默认全局;'article' => 仅文章页①; 'custom' => 仅自定义页① + * pageBshowMode: string, 页面底部-显示方式:未配置默认全局;'article' => 仅文章页①; 'custom' => 仅自定义页① + * + * windowLB: htmlString, 全局左下角② + * windowRB: htmlString, 全局右下角② + * } + * + * ①注:在.md文件front matter配置`article: false`的页面是自定义页,未配置的默认是文章页(首页除外)。 + * ②注:windowLB 和 windowRB:1.展示区块宽高最大是200*200px。2.请给自定义元素定一个不超过200px的固定宽高。3.在屏宽小于960px时无论如何都不会显示。 + */ + +module.exports = { + // homeSidebarB: + // ` + // + // `, + // sidebarT: + // ` + // + // `, + sidebarB: + ` + + `, + // pageT: // + // ` + // + // `, + // pageTshowMode: 'article', + pageB: + ` + + `, + // pageBshowMode: 'article', + // windowLB: // 遮挡部分侧边栏 + // ` + // + // `, + windowRB: + ` + + + `, +} + + +// module.exports = { +// homeSidebarB: `
自定义模块测试
`, +// sidebarT: `
自定义模块测试
`, +// sidebarB: `
自定义模块测试
`, +// pageT: `
自定义模块测试
`, +// pageB: `
自定义模块测试
`, +// windowLB: `
自定义模块测试
`, +// windowRB: `
自定义模块测试
`, +// } diff --git a/docs/.vuepress/config/nav.js b/docs/.vuepress/config/nav.js new file mode 100644 index 00000000..d957e58c --- /dev/null +++ b/docs/.vuepress/config/nav.js @@ -0,0 +1,79 @@ +module.exports = [ + { text: '首页', link: '/' }, + { + text: 'Java', link: '/java/', items: [ + { + text: 'Java-Se', items: [ + { text: '前言', link: '/java/se/preface/' }, + { text: 'Java基础', link: '/java/se/initial-java/' }, + { text: 'Java面向对象', link: '/java/se/object/' }, + { text: 'Java常用类', link: '/java/se/commonly-used-class/' }, + { text: 'Java集合框架', link: '/java/se/aggregate/synopsis/' }, + + ] + }, + { + text: 'Java-Se进阶', items: [ + { text: 'JUC多线程', link: '/java/se/thread/study-note/' }, + + ] + }, + { + text: 'Java-ee', items: [ + + { text: 'JavaWeb', link: '/javaweb/basic-concepts/' }, + ] + }, + + ] + }, + { + text: 'Web', + items: [ + { text: 'HTML', link: '/html5/overview/' }, + { text: 'CSS', link: '/css/overview/' }, + { text: 'JavaScript', link: '/javascript/overview/' }, + { text: 'vue', link: '/vue/overview/' }, + ] + }, + { + text: '数据库', + items: [ + + { + text: 'SQL 数据库', items: [ + { text: 'MySQL', link: '/mysql/preface/' }, + + ] + }, + + { + text: 'NoSQL 数据库', items: [ + { text: 'Redis', link: '/redis/study-note/1/' }, + { text: 'ElasticSearch', link: '/elasticsearch/note/2/' }, + ] + }, + ] + }, + { + text: '框架', + items: [ + { text: 'MyBatis', link: '/mybatis/study-note/' }, + { text: 'MyBatis-Plus', link: '/mybatis-plus/study-note/' }, + { text: 'Spring', link: '/spring/study-note/' }, + + + + ] + }, + { + text: '其他', + items: [ + { text: 'C语言', link: '/c/' }, + { text: 'Git', link: '/git/' }, + { text: 'SMC&P', link: '/SMC&P/note/' }, + ] + }, + { text: '关于', link: '/about/' }, + { text: '归档', link: '/archives/' } +] \ No newline at end of file diff --git a/docs/.vuepress/config/sidebar.js b/docs/.vuepress/config/sidebar.js new file mode 100644 index 00000000..36bd5de9 --- /dev/null +++ b/docs/.vuepress/config/sidebar.js @@ -0,0 +1,104 @@ +// 此文件没有用到,仅用于测试和侧边栏数据格式的参考。 + +module.exports = { // 侧边栏 + '/01.前端/': [ + { + title: 'JavaScript', + collapsable: false, //是否可折叠,可选的,默认true + children: [ + ['01.JavaScript/01.JavaScript中的名词概念','JavaScript中的名词概念'], + ['01.JavaScript/02.数据类型转换','数据类型转换'], + ['01.JavaScript/03.ES5面向对象','ES5面向对象'], + ['01.JavaScript/04.ES6面向对象','ES6面向对象'], + ['01.JavaScript/05.new命令原理','new命令原理'], + ['01.JavaScript/06.多种数组去重性能对比','多种数组去重性能对比'], + ] + }, + ], + '/02.页面/': [ + { + title: 'html-css', + collapsable: false, + children: [ + ['01.html-css/00.flex布局语法','flex布局语法'], + ['01.html-css/01.flex布局案例-基础','flex布局案例-基础'], + ['01.html-css/02.flex布局案例-骰子','flex布局案例-骰子'], + ['01.html-css/03.flex布局案例-网格布局','flex布局案例-网格布局'], + ['01.html-css/04.flex布局案例-圣杯布局','flex布局案例-圣杯布局'], + ['01.html-css/05.flex布局案例-输入框布局','flex布局案例-输入框布局'], + ['01.html-css/06.CSS3之transform过渡','CSS3之transform过渡'], + ['01.html-css/07.CSS3之animation动画','CSS3之animation动画'], + ] + }, + ], + '/03.技术杂谈/': [ + { + title: '技术杂谈', + collapsable: false, //是否可折叠,可选的,默认true + sidebarDepth: 2, // 深度,可选的, 默认值是 1 + children: [ + ['01.Git使用手册','Git使用手册'], // 同 {path: '01.Git使用手册', title: 'Git使用文档'} + ['02.GitHub高级搜索技巧','GitHub高级搜索技巧'], + ['03.Markdown使用教程','Markdown使用教程'], + ['04.npm常用命令','npm常用命令'], + ['05.yaml语言教程','yaml语言教程'], + ['06.解决百度无法收录搭建在GitHub上的个人博客的问题','解决百度无法收录搭建在GitHub上的个人博客的问题'], + ['07.使用Gitalk实现静态博客无后台评论系统','使用Gitalk实现静态博客无后台评论系统'], + ] + } + ], + '/04.其他/': [ + { + title: '学习', + collapsable: false, + children: [ + ['01.学习/01.学习网站','学习网站'], + ['01.学习/02.学习效率低,忘性很大怎么办?','学习效率低,忘性很大怎么办?'], + ] + }, + { + title: '学习笔记', + collapsable: false, + children: [ + ['02.学习笔记/01.小程序笔记','小程序笔记'], + ] + }, + { + title: '面试', + collapsable: false, //是否可折叠,可选的,默认true + children: [ + ['03.面试/01.面试问题集锦','面试问题集锦'], + ] + }, + ['01.在线工具','在线工具'], + ['02.友情链接','友情链接'], + ], + // '/': [ // 在最后定义,在没有单独设置侧边栏时统一使用这个侧边栏 + // '', + // 'git', + // 'github', + // 'markdown', + // 'study', + // 'interview' + // // '/', + // // { + // // title: 'foo', // 标题,必要的 + // // path: '/foo/', // 标题的路径,可选的, 应该是一个绝对路径 + // // collapsable: false, // 是否可折叠,可选的,默认true + // // sidebarDepth: 1, // 深度,可选的, 默认值是 1 + // // children: [ + // // ['foo/', '子页1'], + // // 'foo/1', + // // 'foo/2', + // // ] + // // }, + // // { + // // title: 'bar', + // // children: [ + // // ['bar/', '子页2'], + // // 'bar/3', + // // 'bar/4', + // // ] + // // } + // ], +} \ No newline at end of file diff --git a/docs/.vuepress/enhanceApp.js b/docs/.vuepress/enhanceApp.js new file mode 100644 index 00000000..1589fa48 --- /dev/null +++ b/docs/.vuepress/enhanceApp.js @@ -0,0 +1,9 @@ +// import vue from 'vue/dist/vue.esm.browser' +export default ({ + Vue, // VuePress 正在使用的 Vue 构造函数 + options, // 附加到根实例的一些选项 + router, // 当前应用的路由实例 + siteData // 站点元数据 +}) => { + // window.Vue = vue // 使页面中可以使用Vue构造函数 (使页面中的vue demo生效) +} diff --git a/docs/.vuepress/plugins/love-me/index.js b/docs/.vuepress/plugins/love-me/index.js new file mode 100644 index 00000000..674294c2 --- /dev/null +++ b/docs/.vuepress/plugins/love-me/index.js @@ -0,0 +1,12 @@ +const path= require('path'); +const LoveMyPlugin = (options={}) => ({ + define () { + const COLOR = options.color || "rgb(" + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + ")" + const EXCLUDECLASS = options.excludeClassName || '' + return {COLOR, EXCLUDECLASS} + }, + enhanceAppFiles: [ + path.resolve(__dirname, 'love-me.js') + ] +}); +module.exports = LoveMyPlugin; diff --git a/docs/.vuepress/plugins/love-me/love-me.js b/docs/.vuepress/plugins/love-me/love-me.js new file mode 100644 index 00000000..f93855e6 --- /dev/null +++ b/docs/.vuepress/plugins/love-me/love-me.js @@ -0,0 +1,62 @@ +export default () => { + if (typeof window !== "undefined") { + (function(e, t, a) { + function r() { + for (var e = 0; e < s.length; e++) s[e].alpha <= 0 ? (t.body.removeChild(s[e].el), s.splice(e, 1)) : (s[e].y--, s[e].scale += .004, s[e].alpha -= .013, s[e].el.style.cssText = "left:" + s[e].x + "px;top:" + s[e].y + "px;opacity:" + s[e].alpha + ";transform:scale(" + s[e].scale + "," + s[e].scale + ") rotate(45deg);background:" + s[e].color + ";z-index:99999"); + requestAnimationFrame(r) + } + function n() { + var t = "function" == typeof e.onclick && e.onclick; + + e.onclick = function(e) { + // 过滤指定元素 + let mark = true; + EXCLUDECLASS && e.path && e.path.forEach((item) =>{ + if(item.nodeType === 1) { + typeof item.className === 'string' && item.className.indexOf(EXCLUDECLASS) > -1 ? mark = false : '' + } + }) + + if(mark) { + t && t(), + o(e) + } + } + } + function o(e) { + var a = t.createElement("div"); + a.className = "heart", + s.push({ + el: a, + x: e.clientX - 5, + y: e.clientY - 5, + scale: 1, + alpha: 1, + color: COLOR + }), + t.body.appendChild(a) + } + function i(e) { + var a = t.createElement("style"); + a.type = "text/css"; + try { + a.appendChild(t.createTextNode(e)) + } catch(t) { + a.styleSheet.cssText = e + } + t.getElementsByTagName("head")[0].appendChild(a) + } + // function c() { + // return "rgb(" + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + ")" + // } + var s = []; + e.requestAnimationFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || + function(e) { + setTimeout(e, 1e3 / 60) + }, + i(".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}"), + n(), + r() + })(window, document) + } +} \ No newline at end of file diff --git a/docs/.vuepress/public/img/bg.jpeg b/docs/.vuepress/public/img/bg.jpeg new file mode 100644 index 00000000..85e53e7d Binary files /dev/null and b/docs/.vuepress/public/img/bg.jpeg differ diff --git a/docs/.vuepress/public/img/bg.jpg b/docs/.vuepress/public/img/bg.jpg new file mode 100644 index 00000000..f093e799 Binary files /dev/null and b/docs/.vuepress/public/img/bg.jpg differ diff --git a/docs/.vuepress/public/img/favicon.ico b/docs/.vuepress/public/img/favicon.ico new file mode 100644 index 00000000..10bed8fe Binary files /dev/null and b/docs/.vuepress/public/img/favicon.ico differ diff --git a/docs/.vuepress/public/img/logo.png b/docs/.vuepress/public/img/logo.png new file mode 100644 index 00000000..3c27e5a3 Binary files /dev/null and b/docs/.vuepress/public/img/logo.png differ diff --git a/docs/.vuepress/public/img/more.png b/docs/.vuepress/public/img/more.png new file mode 100644 index 00000000..830613ba Binary files /dev/null and b/docs/.vuepress/public/img/more.png differ diff --git a/docs/.vuepress/public/img/other.png b/docs/.vuepress/public/img/other.png new file mode 100644 index 00000000..87f80989 Binary files /dev/null and b/docs/.vuepress/public/img/other.png differ diff --git a/docs/.vuepress/public/img/panda-waving.png b/docs/.vuepress/public/img/panda-waving.png new file mode 100644 index 00000000..20246c60 Binary files /dev/null and b/docs/.vuepress/public/img/panda-waving.png differ diff --git a/docs/.vuepress/public/img/python.png b/docs/.vuepress/public/img/python.png new file mode 100644 index 00000000..c3ddebeb Binary files /dev/null and b/docs/.vuepress/public/img/python.png differ diff --git a/docs/.vuepress/public/img/ui.png b/docs/.vuepress/public/img/ui.png new file mode 100644 index 00000000..617c56d7 Binary files /dev/null and b/docs/.vuepress/public/img/ui.png differ diff --git a/docs/.vuepress/public/img/web.png b/docs/.vuepress/public/img/web.png new file mode 100644 index 00000000..3c27e5a3 Binary files /dev/null and b/docs/.vuepress/public/img/web.png differ diff --git a/docs/.vuepress/styles/index.styl b/docs/.vuepress/styles/index.styl new file mode 100644 index 00000000..36eac481 --- /dev/null +++ b/docs/.vuepress/styles/index.styl @@ -0,0 +1,60 @@ +.article-list + max-width $homePageWidth!important + +// 评论区颜色重置 +.gt-container + .gt-ico-tip + &::after + content: '。( Win + . ) or ( ⌃ + ⌘ + ␣ ) open Emoji' + color: #999 + .gt-meta + border-color var(--borderColor)!important + .gt-comments-null + color var(--textColor) + opacity .5 + .gt-header-textarea + color var(--textColor) + background rgba(180,180,180,0.1)!important + .gt-btn + border-color $accentColor!important + background-color $accentColor!important + .gt-btn-preview + background-color rgba(255,255,255,0)!important + color $accentColor!important + a + color $accentColor!important + .gt-svg svg + fill $accentColor!important + .gt-comment-content,.gt-comment-admin .gt-comment-content + background-color rgba(150,150,150,0.1)!important + &:hover + box-shadow 0 0 25px rgba(150,150,150,.5)!important + .gt-comment-body + color var(--textColor)!important + + +// qq徽章 +.qq + position: relative; +.qq::after + content: "可撩"; + background: $accentColor; + color:#fff; + padding: 0 5px; + border-radius: 10px; + font-size:12px; + position: absolute; + top: -4px; + right: -35px; + transform:scale(0.85); + +// demo模块图标颜色 +body .vuepress-plugin-demo-block__wrapper + &,.vuepress-plugin-demo-block__display + border-color rgba(160,160,160,.3) + .vuepress-plugin-demo-block__footer:hover + .vuepress-plugin-demo-block__expand::before + border-top-color: $accentColor !important; + border-bottom-color: $accentColor !important; + svg + fill: $accentColor !important; diff --git a/docs/.vuepress/styles/palette.styl b/docs/.vuepress/styles/palette.styl new file mode 100644 index 00000000..d8f277fd --- /dev/null +++ b/docs/.vuepress/styles/palette.styl @@ -0,0 +1,62 @@ + +// 原主题变量已弃用,以下是vdoing使用的变量,你可以在这个文件内修改它们。 + +//***vdoing主题-变量***// + +// // 颜色 + +// $bannerTextColor = #fff // 首页banner区(博客标题)文本颜色 +// $accentColor = #11A8CD +// $arrowBgColor = #ccc +// $badgeTipColor = #42b983 +// $badgeWarningColor = darken(#ffe564, 35%) +// $badgeErrorColor = #DA5961 + +// // 布局 +// $navbarHeight = 3.6rem +// $sidebarWidth = 18rem +// $contentWidth = 860px +// $homePageWidth = 1100px +// $rightMenuWidth = 230px // 右侧菜单 + +// // 代码块 +// $lineNumbersWrapperWidth = 2.5rem + +// // 浅色模式 +// body,.theme-mode-light +// --bodyBg: #f4f4f4 +// --mainBg: rgba(255,255,255,1) +// --sidebarBg: rgba(255,255,255,.8) +// --blurBg: rgba(255,255,255,.9) +// --textColor: #004050 +// --textLightenColor: #0085AD +// --borderColor: rgba(0,0,0,.15) +// --codeBg: #f6f6f6 +// --codeColor: #525252 +// codeThemeLight() + +// // 深色模式 +// .theme-mode-dark +// --bodyBg: rgb(39,39,43) +// --mainBg: rgba(30,30,34,1) +// --sidebarBg: rgba(30,30,34,.8) +// --blurBg: rgba(30,30,34,.8) +// --textColor: rgb(140,140,150) +// --textLightenColor: #0085AD +// --borderColor: #2C2C3A +// --codeBg: #252526 +// --codeColor: #fff +// codeThemeDark() + +// // 阅读模式 +// .theme-mode-read +// --bodyBg: rgb(240,240,208) +// --mainBg: rgba(245,245,213,1) +// --sidebarBg: rgba(245,245,213,.8) +// --blurBg: rgba(245,245,213,.9) +// --textColor: #004050 +// --textLightenColor: #0085AD +// --borderColor: rgba(0,0,0,.15) +// --codeBg: #282c34 +// --codeColor: #fff +// codeThemeDark() \ No newline at end of file diff --git "a/docs/00.\347\233\256\345\275\225\351\241\265/01.java.md" "b/docs/00.\347\233\256\345\275\225\351\241\265/01.java.md" new file mode 100644 index 00000000..2b1541ba --- /dev/null +++ "b/docs/00.\347\233\256\345\275\225\351\241\265/01.java.md" @@ -0,0 +1,16 @@ +--- +pageComponent: + name: Catalogue + data: + key: 01.Java + imgUrl: /img/web.png + description: java笔记 +title: java页面 +date: 2021年4月14日14:39:06 +permalink: /java +sidebar: false +article: false +comment: false +editLink: false +--- + diff --git "a/docs/00.\347\233\256\345\275\225\351\241\265/10.\345\205\263\344\272\216.md" "b/docs/00.\347\233\256\345\275\225\351\241\265/10.\345\205\263\344\272\216.md" new file mode 100644 index 00000000..6d3e77bf --- /dev/null +++ "b/docs/00.\347\233\256\345\275\225\351\241\265/10.\345\205\263\344\272\216.md" @@ -0,0 +1,15 @@ +--- +pageComponent: + name: Catalogue + data: + key: 10.关于 + imgUrl: /img/web.png + description: 关于 +title: 关于 +permalink: /about +sidebar: false +article: false +comment: false +editLink: false +date: 2021-05-16 22:59:58 +--- \ No newline at end of file diff --git "a/docs/00.\347\233\256\345\275\225\351\241\265/50.c\350\257\255\350\250\200.md" "b/docs/00.\347\233\256\345\275\225\351\241\265/50.c\350\257\255\350\250\200.md" new file mode 100644 index 00000000..8a27a263 --- /dev/null +++ "b/docs/00.\347\233\256\345\275\225\351\241\265/50.c\350\257\255\350\250\200.md" @@ -0,0 +1,15 @@ +--- +pageComponent: + name: Catalogue + data: + key: 50.C语言 + imgUrl: /img/web.png + description: c语言笔记 +title: c语言页面 +permalink: /c +sidebar: false +article: false +comment: false +editLink: false +date: 2021-05-09 13:57:16 +--- diff --git "a/docs/00.\347\233\256\345\275\225\351\241\265/51.Git.md" "b/docs/00.\347\233\256\345\275\225\351\241\265/51.Git.md" new file mode 100644 index 00000000..d417a846 --- /dev/null +++ "b/docs/00.\347\233\256\345\275\225\351\241\265/51.Git.md" @@ -0,0 +1,15 @@ +--- +pageComponent: + name: Catalogue + data: + key: 51.Git + imgUrl: /img/web.png + description: Git目录页面 +title: Git目录页面 +permalink: /git +sidebar: false +article: false +comment: false +editLink: false +date: 2021-05-15 09:16:46 +--- diff --git "a/docs/01.Java/01.\345\211\215\350\250\200.md" "b/docs/01.Java/01.\345\211\215\350\250\200.md" new file mode 100644 index 00000000..99ce5570 --- /dev/null +++ "b/docs/01.Java/01.\345\211\215\350\250\200.md" @@ -0,0 +1,45 @@ +--- +title: 前言 +date: 2021-05-5 22:39:56 +permalink: /java/se/preface/ +categories: + - java + - java-se +--- + + + +## 参考资料 + +学习java路线:https://www.bilibili.com/read/cv5216534 + +狂神视频地址:https://space.bilibili.com/95256449/ + + + +## 网站搭建 + +### 如何搭建 + + + +GitHub:[https://github.com/oddfar/docs](https://github.com/oddfar/docs) + + + +### 为何不用博客 + +本人博客:https://oddfar.com + +搭建博客需要服务器+域名,在阿里云购买学生机即可 + +写博客个人推荐使用typecho,好看的主题有[handsome](https://www.ihewro.com/archives/489/)(收费88)、[joe](https://typecho.me/1520.html)(免费) + +最开始我是用的博客做笔记,虽然有很多功能,后来博客页面美化的越来越“花里胡哨”,不适合进行大文章的阅读,以及不能更快更好的查看知识,后来发现了vuepress,特别适合做文档或知识体系,于是就选择了它。 + + + + + + + diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/01.\345\210\235\350\257\206java.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/01.\345\210\235\350\257\206java.md" new file mode 100644 index 00000000..ab25b3df --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/01.\345\210\235\350\257\206java.md" @@ -0,0 +1,526 @@ +--- +title: 初识java +date: 2021-04-15 22:39:56 +permalink: /java/se/initial-java/ +categories: + - java + - java-se +--- +# 初始java + +## Java : 一个帝国的诞生 + +### 1、C语言帝国的统治 + +现在是公元1995年, C语言帝国已经统治了我们20多年, 实在是太久了。 + + + +1972年, 随着C语言的诞生和Unix的问世, 帝国迅速建立统治, 从北美到欧洲, 从欧洲到亚洲, 无数程序员臣服在他的脚下。 + + + +帝国给我们提供了极好的福利:贴近硬件, 运行极快, 效率极高。 + + + +使用这些福利, 程序员们用C 开发了很多系统级软件,操作系统, 编译器, 数据库,网络系统..... + + + +但是帝国也给我们安上了两个沉重的枷锁: 指针和内存管理 + + + +虽然指针无比强大, 能直接操作内存, 但是帝国却没有给我们工具去做越界的检查, 导致很多新手程序员轻易犯错。 + + + +至于内存管理, 帝国更完全是放任的态度: 你自己分配的空间, 自己去释放 ! + + + +更要命的是这些问题在编译期发现不了, 在运行时才会突然暴露, 常常让我们手忙脚乱, 昏天黑地去调试。 + + + +我们的大量时间和宝贵的精力都被浪费在小心翼翼的处理指针和内存分配上。 + + + +每个程序员都被这两个东西搞的焦头烂额! + + + +帝国宣称的可移植性骗了我们,他宣称我们在一个机器上写的程序, 只要在另外一个机器上编译就可以了, 实际上不是这样。 他要求我们尽量用标准的C函数库。其次,如果遇到了一些针对特定平台的调用, 需要对每个平台都得写一份 ! 有一点点小错误,都会导致编译失败。 + + + +1982年,帝国又推出了一门新的语言C++, 添加了面向对象的功能,兼容C, 有静态类型检查, 性能也很好。 + + + +但是这门新的语言实在是太复杂了, 复杂到比我聪明的多的人都没有办法完全掌握这门语言,它的很多特性复杂的让人吃惊。 + + + +C++在图形领域和游戏上取得了一些成功, 但是我一直学不好它。 + +### 2、反抗 + +我决定反抗这个庞大的帝国, 我偷偷的带领着一帮志同道合的兄弟离开了,我们要新建一块清新自由的领地。 + + + +为了吸引更多的程序员加入我们, 我们要建立一个新的语言,这个语言应该有这样的特性: + + + +语法有点像C , 这样大家容易接受 + +没有C语言那样的指针 + +再也不要考虑内存管理了, 实在受不了了 + +真正的可移植性, 编写一次, 到处运行 + +面向对象 + +类型安全 + +还有,我们要提供一套高质量的类库, 随语言发行。 + + + +我想把这个语言命名为C++-- , 即C++减减, 因为我想在C++的基础上改进,把它简化。 + + + +后来发现不行, 设计理念差别太大。 + + + +干脆重启炉灶。 + + + +我看到门口的一棵橡树, 就把这个语言叫做Oak。 + +但是后来发布的时候, 发现Oak已经被别人用了, 我们讨论很久, 最终决定把这门新的语言叫做 Java。 + + + +为了实现跨平台, 我们在操作系统和应用程序之间增加了一个抽象层: Java 虚拟机 + + + +用Java写的程序都跑在虚拟机上, 除非个别情况, 都不用看到操作系统。 + +### 3、一鸣惊人 + +为了吸引更多的人加入我们的新领地, 我们决定搞一个演示, 向大家展示Java 的能力。 + + + +出世未久的Java其实还远不完善。 搞点什么好呢? + + + +我们把眼光盯上了刚刚兴起的互联网, 1995年的网页简单而粗糙, 缺乏互动性。 于是我们在浏览器上弄了个小插件, 把java 运行环境放了上去。 + + + +然后在上面开发了一个图形界面的程序(Applet), 让它看起来美轮美奂, 震撼人心。 + + + +每一个看到他的程序员都会发出“Wow”的惊叹 !为之倾倒。 + + + +Java 活了! + + + +通过Applet , 无数的程序员看到了Java这门语言,了解了这门语言特性以后, 很多无法忍受C帝国暴政的程序员, 很快加入了我们, 我们的领地开始迅速扩大。 + + + +连C语言帝国里的一些商业巨头也纷纷来和我们合作, 其中就包括Oracle , 微软这样的巨头 , 微软的头领Bill Gates还说 :这是迄今为止设计的最好的语言! + + + +但是Bill Gates非常的不地道, 买了我们的Java 许可以后,虽然在自家的浏览器上也支持Applet, 但是他们却偷偷的试图修改Java , 想把Java绑死在自家的操作系统上赚钱, Java会变的不可移植。 + + + +这是我们难于忍受的, 我们和微软发起了一场旷日持久的游击战争, 逼着微软退出了Java领域, 开发了自己的.NET , 这是后话。 + +### 4、开拓疆土 + +从1995年到1997年,我们依靠 Java 不断的攻城略地, 开拓疆土,我们王国的子民不断增加, 达到了几十万之众, 已经是一个不可忽视的力量了。 + + + +但是大家发现, Java除了Applet, 以及一些小程序之外, 似乎干不了别的事情。 + + + +C帝国的人还不断的嘲笑我们慢, 像个玩具。 + + + +到了1998年, 经过密谋, 我们Java 王国决定派出三只军队向外扩展: + +Java 2 标准版(J2SE): 去占领桌面 + +Java 2 移动版(J2ME): 去占领手机 + +Java 2 企业版(J2EE): 去占领服务器 + + + +其中的两只大军很快败下阵来。 + + + +J2SE 的首领发现, 开发桌面应用的程序员根本接受不了Java, 虽然我们有做的很优雅的Swing 可以开发界面, 但是开发出的界面非常难看, 和原生的桌面差距很大。 尤其是为了运行程序还得安装一个虚拟机, 大家都受不了。 + + + +J2ME也是, 一直不受待见, 当然更重要的原因是乔布斯还没有重新发明手机, 移动互联网还没有启动。 + + + +失之东隅,收之桑榆, J2EE赶上了好时候, 互联网大发展, 大家忽然发现, Java简直是为写服务器端程序所发明的! + + + +强大, 健壮, 安全, 简单, 跨平台 ! + + + +在J2EE规范的指导下, 特别适合团队开发复杂的大型项目。 + + + +我们授权BEA公司第一个使用J2EE许可证, 推出了Weblogic, 凭借其集群功能, 第一次展示了复杂应用的可扩展性和高可用性。 + + + +这个后来被称为中间件的东西把程序员从事务管理,安全管理,权限管理等方面解放出来, 让他们专注于业务开发。 这立刻捕获了大量程序员的心。 + + + +很快Java 王国的子民就达到数百万之众。 + + + +榜样的力量是无穷的, 很快其他商业巨头也纷纷入场, 尤其是IBM,在Java 上疯狂投入,不仅开发了自己的应用服务器 Websphere, 还推出了Eclipse这个极具魅力的开源开发平台。 + + + +当然IBM利用java 获得了非常可观的效益, 软件+硬件+服务 三驾马车滚滚向前, 把IBM推向了一个新的高峰。 + +### 5、帝国的诞生 + +大家也没有想到,除了商业巨头以外, 程序员们也会对Java王国 这么热爱, 他们基于Java 开发了巨多的平台,系统,工具,例如: + + + +构建工具: Ant,Maven, Jekins + + + +应用服务器: Tomcat,Jetty, Jboss, Websphere, weblogic + + + +Web开发: Struts,Spring,Hibernate, myBatis + + + +开发工具: Eclipse, Netbean,intellij idea, Jbuilder + +。。。。等等等等。。。。 + + + +并且绝大部分都是开源的 ! + + + +微软眼睁睁的看着服务器端的市场被Java 王国占据, 岂能善罢甘休? 他们赶紧推出.NET来对抗, 但我们已经不在乎了, 因为他的系统是封闭的,所有的软件都是自家的: + +开发工具是Visual Studio, 应用服务器是IIS, 数据库是SQL Server,只要你用.NET,基本上就会绑定微软。 + + + +另外他们的系统只能运行在Windows服务器上, 这个服务器在高端市场的占有率实在是太低了。 + + + +2005年底, 一个新的王国突然崛起, 他们号称开发效率比java 快5-10倍, 由此吸引了大批程序员前往加盟。 + + + +这个新的王国叫做Ruby on Rails, 它结合了PHP体系的优点(快速开发)和Java体系的优点(程序规整), 特别适合快速的开发简单的Web网站。 + + + +虽然发展很快, 但没有对Java 王国产生实质性的威胁, 使用Ruby on Rails搭建大型商业系统的还很少。 + + + +除了Ruby on Rails ,还有PHP, Python , 都适合快速开发不太复杂的Web系统。 但是关键的,复杂的商业系统开发还是Java 王国的统治之下。 所以我们和他们相安无事。 + + + +2006年, 一只叫Hadoop的军队让Java王国入侵了大数据领域, 由于使用Java 语言, 绝大多数程序员在理解了Map/Reduce , 分布式文件系统在Hadoop中的实现以后, 很快就能编写处理处理海量数据的程序, Java 王国的领地得到了极大的扩展。 + + + +2008年, 一个名叫Android 的系统横空出世, 并且随着移动互联网的爆发迅速普及, 运行在Android之上的正是Java ! + + + +Java 王国在Google的支持下, 以一种意想不到的方式占领了手机端, 完成了当年J2ME 壮志未酬的事业 ! + + + +到今年为止, 全世界估计有1000万程序员加入了Java王国,它领土之广泛, 实力之强大, 是其他语言无法比拟的。 + + + +Java 占据了大部分的服务器端开发,尤其是关键的复杂的系统, 绝大多数的手机端, 以及大部分的大数据领域。 + + + +一个伟大的帝国诞生了。 + + + +## Java的特性和优势 + +#### 八大特性 + +**1、跨平台/可移植性** + +这是Java的核心优势。Java在设计时就很注重移植和跨平台性。比如:Java的int永远都是32位。不像C++可能是16,32,可能是根据编译器厂商规定的变化。这样的话程序的移植就会非常麻烦。 + + **2、安全性** + +Java适合于网络/分布式环境,为了达到这个目标,在安全性方面投入了很大的精力,使Java可以很容易构建防病毒,防篡改的系统。 + + **3、面向对象** + +面向对象是一种程序设计技术,非常适合大型软件的设计和开发。由于C++为了照顾大量C语言使用者而兼容了C,使得自身仅仅成为了带类的C语言,多少影响了其面向对象的彻底性! + +Java则是完全的面向对象语言。 + +**4、简单性** + +Java就是C++语法的简化版,我们也可以将Java称之为“C++-”。跟我念“C加加减”,指的就是将C++的一些内容去掉;比如:头文件,指针运算,结构,联合,操作符重载,虚基类等等。 + +同时,由于语法基于C语言,因此学习起来完全不费力。 + + **5、高性能** + +Java最初发展阶段,总是被人诟病“性能低”;客观上,高级语言运行效率总是低于低级语言的,这个无法避免。Java语言本身发展中通过虚拟机的优化提升了几十倍运行效率。 + +比如,通过JIT(JUST IN TIME)即时编译技术提高运行效率。 将一些“热点”字节码编译成本地机器码,并将结果缓存起来,在需要的时候重新调用。这样的话,使Java程序的执行效率大大提高, + +某些代码甚至接待C++的效率。因此,Java低性能的短腿,已经被完全解决了。业界发展上,我们也看到很多C++应用转到Java开发,很多C++程序员转型为Java程序员。 + + **6、分布式** + +Java是为Internet的分布式环境设计的,因为它能够处理TCP/IP协议。事实上,通过URL访问一个网络资源和访问本地文件是一样简单的。Java还支持远程方法调用(RMI,Remote Method Invocation), + +使程序能够通过网络调用方法。 + + **7、多线程** + +多线程的使用可以带来更好的交互响应和实时行为。 Java多线程的简单性是Java成为主流服务器端开发语言的主要原因之一。 + + **8、健壮性** + +Java是一种健壮的语言,吸收了C/C++ 语言的优点,但去掉了其影响程序健壮性的部分(如:指针、内存的申请与释放等)。Java程序不可能造成计算机崩溃。即使Java程序也可能有错误。 + +如果出现某种出乎意料之事,程序也不会崩溃,而是把该异常抛出,再通过异常处理机制加以处理。 + +#### 核心优势 + +![img](https://cdn.jsdelivr.net/gh/oddfar/static/img/初识java.assets/v2-9992d6134d870ebd7d6b93c11440329d_720w.jpg) + +  跨平台是Java语言的核心优势,赶上最初互联网的发展,并随着互联网的发展而发展,建立了强大的生态体系,目前已经覆盖IT各行业的“第一大语言”,是计算机界的“英语”。 + +  虽然,目前也有很多跨平台的语言,但是已经失去先机,无法和Java强大的生态体系抗衡。Java仍将在未来几十年成为编程语言的主流语言。 + +JAVA虚拟机是JAVA实现跨平台的核心。事实上,基于JAVA虚拟机(JVM)的编程语言还有很多种: + +![img](https://cdn.jsdelivr.net/gh/oddfar/static/img/初识java.assets/v2-646ad77cd0889ca096285ba5f7e4f3ee_720w.jpg) + +基于JAVA生态建立的产品将会越来越多;基于JAVA虚拟机的编程语言也将会越来越多;生态系统的强大,是JAVA能长盛不衰的根本。 + +## Java三大版本 + +**JAVA最大的特点:** + + Java的主要优势在于其做出的WORA:即一次编写(Write Once)、随处运行(Run Anywhere)。简单来讲,这意味着开发团队能够利用Java编写一款应用程序,并将其编译为可执行形式,而后将其运行 在任何支持Java的平台之上。这显然能够极大提高编程工作的实际效率,这种优势来源于Java Virtual Machine(JAVA虚拟机的缩写),JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在 实际的计算机上仿真模拟各种计算机功能来实现的。JAVA语言的一个非常重要的特点就是与平台的无关 性,而使用Java虚拟机是实现这一特点的关键。 + +**JAVA三大版本:** + +1. JAVA SE:它是JAVA的标准版,是整个JAVA的基础和核心,这是我们主要学习的一个部分,也是 JAVAEE和JAVAME技术的基础,主要用于开发桌面应用程序。学会后可以做一些简单的桌面应用 如:扫雷,连连看等。 +2. JAVA ME:它是JAVA的微缩版,主要应用于嵌入式开发,比如手机程序的开发。目前来说就业范围不是很广,在一些城市可能相对的不好找工作。 +3. JAVA EE:也叫JAVA的企业版,它提供了企业级应用开发的完整解决方案,比如开发网站,还有企业的一些应用系统,是JAVA技术应用最广泛的领域。主要还是偏向于WEB的开发,而JAVA EE的基础就是JAVA SE,所以我们在学习JAVA SE的时候,基础一定要打好,因为这是最基本的,也是最核 心的。 + +## JDK 和 JRE + +**JDK** + +Java 2 SDK (Development Kit)包含:JRE的超集,包含编译器和调试器等用于程序开发的文件 + +**JRE** + +Java Runtime Environment (JRE) 包含:Java虚拟机、库函数、运行Java应用程序和Applet所必须文件 + +Java运行环境的三项主要功能: + +- 加载代码:由class loader 完成; +- 校验代码:由bytecode verifier 完成; +- 执行代码:由 runtime interpreter完成。 + +**区别和联系**: + +sdk(也就是jdk)是jre的超集,是在jre的基础上增加了编译器及其他一些开发工具。 + +jre就是java运行时环境,包括了jvm和其它一些java核心api,任何一台电脑,只有安装了jre才可以行 java程序. + +如果只是要运行JAVA程序,之需要JRE就可以。 JRE通常非常小,也包含了JVM. + +如果要开发JAVA程序,就需要安装JDK。 + +## 初识JVM + +JVM(JAVA Virtual Machine) + +JVM是一种规范,可以使用软件来实现,也可以使用硬件来实现,就是一个虚拟的用于执byte-codes 字节码的计算机。他也定义了指令集、寄存器集、结构栈、垃圾收集堆、内存区域。 + +JVM负责将java字节码解释运行,边解释边运行,这样,速度就会受到一定的影响。JAVA提供了另一种 解释运行的方法JIT(just in time),可以一次解释完,再运行特定平台上的机器码,高级的JIT可以只能 分析热点代码,并将这些代码转成本地机器码,并将结果缓存起来,下次直接从内存中调用,这样就大 大提高了执行JAVA代码的效率。这样就实现了跨平台、可移植的功能。 + +1. JVM是指在一台计算机上由软件或硬件模拟的计算机;它类似一个小巧而高效的CPU。 + +2. byte-code代码是与平台无关的是虚拟机的机器指令。 + +3. java字节代码运行的两种方式: + + - interpreter(解释) + + 运行期解释字节码并执行 + + - Just-in-time(即时编译) + + 由代码生成器将字节代码转换成本机的机器代码,然后可以以较高速度执行。 + +[^初识JVM]: https://www.jianshu.com/p/17ab51b87a13 +JAVA的跨平台实现的核心是不同平台使用不同的虚拟机 + +不同的操作系统有不同的虚拟机。Java 虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,随处运行”。 + + + +## JAVA程序运行机制 + +说到Java的运行机制,不得不提一下什么是编译型语言,什么是解释型语言。 + +### **编译型语言** + +编译型语言是先将源代码编译成机器语言(机器可以读懂的语言),再由机器运行机器码,这样执行程序的效率比较高。像C和C++就是典型的编译型语言。 + +### **解释型语言** + +其实解释型语言是相对编译型语言存在的,解释型语言是在运行的时候才进行编译,每次运行都需要编译,这样效率比较低。像JavaScript,Python就是典型的解释型语言 + +### **二者的区别** + +简单的举个例子:同样一本英文书,找人翻译成中文版的书然后拿给你看就是编译,找一个翻译员在你旁边给你解读书的含义就是解释。两者各有利弊,编译型语言执行效率高,翻译一次可以多次运行。解释性语言执行效率低,每次运行都需要重新翻译。但是解释型的跨平台性相对要好,比如解释给一个懂中文和解释给一个懂日文的人就叫做兼容性。 + +### **Java的运行机制** + +Java属于两者都有,既有编译过程,又是解释型语言 + +Java语言虽然比较接近解释型语言的特征,但在执行之前已经预先进行一次预编译,生成的代码是介 于机器码和Java源代码之间的中介代码,运行的时候则由JVM(Java的虚拟机平台,可视为解释器)解 释执行。它既保留了源代码的高抽象、可移植的特点,又已经完成了对源代码的大部分预编译工作,所以 执行起来比“纯解释型”程序要快许多。 + +总之,随着设计技术与硬件的不断发展,编译型与解释型两种方式的界限正在不断变得模糊。 + +#### 第一步:编译 + +利用编译器(javac)将源程序编译成字节码à 字节码文件名:源文件名.class + +#### 第二部:运行 + +利用虚拟机(解释器,java)解释执行class字节码文件。 + +![image-20210319180207773](https://cdn.jsdelivr.net/gh/oddfar/static/img/初识java.assets/image-20210319180207773.png) + + +## Hello World + +需要先配置好开发环境 + +参考链接:https://www.runoob.com/java/java-environment-setup.html + +测试代码一定要写HelloWorld!代表你向这个世界的呐喊,仪式感很重要,就像你生活 中和家人,朋友,妻子在节日中或者纪念日一定要做一些事情,这就是仪式感。 + +1. 新建文件 Hello.java + +2. 编写我们的HelloWorld程序! + + ```java + public class Hello{ + public static void main(String[] args){ + System.out.println("Hello,World!"); + } + } + ``` + +3. 保存文件,cmd打开命令行,利用javac编译! + + ```cmd + javac Hello.java + # 如果没有报错,查看文件夹下是否有新的一个文件 + # Hello.class + # 如果没有出现,恭喜!说明你遇到了你在学Java当中的第一个Bug + ``` + +4. java 执行! + + ```cdm + java Hello + # 成功输出Hello,World! + ``` + +如果出现错误,检查字母大小写是否有错误,或者是否标点符号错误,文件名错误等等,一定要确保成功输出 + +**编写 Java 程序时,应注意以下几点:** + +- 大小写敏感 + + Java 是大小写敏感的,这就意味着标识符 Hello 与 hello 是不同的。 + +- 类名 + + 对于所有的类来说,类名的首字母应该大写。如果类名由若干单词组成,那么每个单词的首字母应该大写,例如 MyFirstJavaClass 。 + +- 方法名 + + 所有的方法名都应该以小写字母开头。如果方法名含有若干单词,则后面的每个单词首字 母大写。 + +- 源文件名 + + 源文件名必须和类名相同。当保存文件的时候,你应该使用类名作为文件名保存(切记 Java 是大小写敏感的),文件名的后缀为 .java。(如果文件名和类名不相同则会导致编译错误)。 + +- 主方法入口 + + 所有的 Java 程序由 public static void main(String []args) 方法开始执行。 + diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/01.\346\263\250\351\207\212.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/01.\346\263\250\351\207\212.md" new file mode 100644 index 00000000..893fcd7c --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/01.\346\263\250\351\207\212.md" @@ -0,0 +1,62 @@ +--- +title: 注释 +date: 2021-04-15 22:28:45 +permalink: /java/se/basic-grammar/annotation +categories: + - java + - java-se +--- +# JavaSE-基础语法 + +## 注释 + +平时我们编写代码,在代码量比较少的时候,我们还可以看懂自己写的,但是当项目结构一旦复杂起来,我们就需要用到一个注释了,注释就类似于我们上学时候写的笔记,我们看着笔记就知道自己写的 什么东西了!在程序中也是如此。我们来看一下Java中的注释怎么写,看以下代码: + +```java +/* +* @Description HelloWorld类 +* @Author Diamond 狂神 +**/ +public class HelloWorld { + /* + 这是我们Java程序的主入口, + main方法也是程序的主线程。 + */ + public static void main(String[] args) { + //输出HelloWorld! + System.out.println("Hello,World!"); + } +} + +``` + +注释并不会被执行,是给我们写代码的人看的,书写注释是一个非常好的习惯。 + +**Java中的注释有三种:** + +单行注释:只能注释当前行,以//开始,直到行结束 + +```java +//输出HelloWorld! +``` + +多行注释:注释一段文字,以/*开始, */结束! + +```java +/* + 这是我们Java程序的主入口, + main方法也是程序的主线程。 +*/ +``` + +文档注释:用于生产API文档,配合JavaDoc。 + +```java +/* +* @Description HelloWorld类 +* @Author Diamond 狂神 +**/ +``` + + + diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/02.\346\240\207\350\257\206\347\254\246.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/02.\346\240\207\350\257\206\347\254\246.md" new file mode 100644 index 00000000..61c4506a --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/02.\346\240\207\350\257\206\347\254\246.md" @@ -0,0 +1,47 @@ +--- +title: 标识符 +date: 2021-04-15 22:28:45 +permalink: /java/se/basic-grammar/identifier/ +categories: + - java + - java-se +--- + +## 标识符 + +每个人从出生开始就有一个名字,咋们生活中的所有事物也都有名字,这名字是谁规定呢?回答是:造物主,谁生产出来的谁规定名字,在我们的程序中也不例外。 + +我们作为造物主,需要给所有的东西给上一个名字,比如我们的HelloWorld程序: + +HelloWorld是类名,也是我们的文件名。它前面的 public class是关键字,不过是搞Java那群人已经定 义好的有特殊作用的,下面的每一个代码都有自己的意思和名字对吧,就是用来作区分!和我们的名字 一样,拿来被叫或者称呼的,程序一切都源自于生活,一定要把学程序和生活中的一切联系起来,你会发现这一切都是息息相关的。 + +![image-20210319190451624](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210319190451624.png) + +我们来看看有哪些是Java自己定义好的关键字呢? + +![image-20210319190649910](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210319190649910.png) + +我们自己起名字有哪些要求呢? + +表示类名的标识符用大写字母开始。 + +> 如:Man, GoodMan + +表示方法和变量的标识符用小写字母开始,后面的描述性词以大写开始。 + +> 如:eat(),eatFood() + +具体可参考《阿里巴巴Java开发手册》 + +**关于 Java 标识符,有以下几点需要注意:** + +- 所有的标识符都应该以字母(A-Z 或者 a-z)美元符($)或者下划线(_)开始 +- 首字符之后可以是字母(A-Z 或者 a-z)美元符($)下划线(_)或数字的任何字符组合 +- 不能使用关键字作为变量名或方法名。 +- 标识符是大小写敏感的 +- 合法标识符举例:age、$salary、_value、__1_value +- 非法标识符举例:123abc、-salary、#abc + +JAVA不采用通常语言使用的ASCII字符集,而是采用unicode这样的标准的国际字符集。因此,这里的 字母的含义:可以表示英文、汉字等等。 + +可以使用中文命名,但是一般不建议这样去使用,也不建议使用拼音,很Low \ No newline at end of file diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/03.\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/03.\346\225\260\346\215\256\347\261\273\345\236\213.md" new file mode 100644 index 00000000..00d994f6 --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/03.\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -0,0 +1,325 @@ +--- +title: 数据类型 +date: 2021-04-15 22:28:45 +permalink: /java/se/basic-grammar/data-type/ +categories: + - java + - java-se +--- + +## 数据类型 + +Java是一种强类型语言,每个变量都必须声明其类型。 + +扩展:各种字符集和编码详解(https://www.cnblogs.com/cmt/p/14553189.html) + +### 1、强弱类型语言 + +- 强类型语言 + + 强类型语言是一种强制类型定义的语言,一旦某一个变量被定义类型,如果不经过强制转换,则它永远就是该数据类型了,强类型语言包括Java、.net 、Python、C++等语言。 + + 举个例子:定义了一个整数,如果不进行强制的类型转换,则不可以将该整数转化为字符串。 + +- 弱类型语言 + + 弱类型语言是一种弱类型定义的语言,某一个变量被定义类型,该变量可以根据环境变化自动进行转换,不需要经过显性强制转换。弱类型语言包括vb 、PHP、javascript等语言。 + + 在VB Script中,可以将字符串‘12’和整数3进行连接得到字符串‘123’,也可以把它看成整数123,而不需 要显示转换。是不是十分的随便,我们Java就不是这样的。 + +- 区别 + + 无论是强类型语言还是弱类型语言,判别的根本是是否会隐性的进行语言类型转变。强类型语言在速度上略逊于弱类型语言,但是强类型定义语言带来的严谨性又能避免不必要的错误。 + +### 2、数据类型 + +Java的数据类型分为两大类:基本类型(primitive type)和引用类型 (reference type) + +![image-20210319191956137](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210319191956137.png) + +【注:引用数据类型的大小统一为4个字节,记录的是其引用对象的地址!】 + +![image-20210319192043592](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210319192043592.png) + +如果你看到这一堆头疼的话,没关系,不用记,JDK中类型对应的包装类都帮忙写好了,我们需要时候可 以直接看到!可以把以下代码拷贝进行查看结果: + +```java +public static void main(String[] args) { + // byte + System.out.println("基本类型:byte 二进制位数:" + Byte.SIZE); + System.out.println("包装类:java.lang.Byte"); + System.out.println("最小值:Byte.MIN_VALUE=" + Byte.MIN_VALUE); + System.out.println("最大值:Byte.MAX_VALUE=" + Byte.MAX_VALUE); + System.out.println(); + // short + System.out.println("基本类型:short 二进制位数:" + Short.SIZE); + System.out.println("包装类:java.lang.Short"); + System.out.println("最小值:Short.MIN_VALUE=" + Short.MIN_VALUE); + System.out.println("最大值:Short.MAX_VALUE=" + Short.MAX_VALUE); + System.out.println(); + // int + System.out.println("基本类型:int 二进制位数:" + Integer.SIZE); + System.out.println("包装类:java.lang.Integer"); + System.out.println("最小值:Integer.MIN_VALUE=" + Integer.MIN_VALUE); + System.out.println("最大值:Integer.MAX_VALUE=" + Integer.MAX_VALUE); + System.out.println(); + // long + System.out.println("基本类型:long 二进制位数:" + Long.SIZE); + System.out.println("包装类:java.lang.Long"); + System.out.println("最小值:Long.MIN_VALUE=" + Long.MIN_VALUE); + System.out.println("最大值:Long.MAX_VALUE=" + Long.MAX_VALUE); + System.out.println(); + // float + System.out.println("基本类型:float 二进制位数:" + Float.SIZE); + System.out.println("包装类:java.lang.Float"); + System.out.println("最小值:Float.MIN_VALUE=" + Float.MIN_VALUE); + System.out.println("最大值:Float.MAX_VALUE=" + Float.MAX_VALUE); + System.out.println(); + // double + System.out.println("基本类型:double 二进制位数:" + Double.SIZE); + System.out.println("包装类:java.lang.Double"); + System.out.println("最小值:Double.MIN_VALUE=" + Double.MIN_VALUE); + System.out.println("最大值:Double.MAX_VALUE=" + Double.MAX_VALUE); + System.out.println(); + // char + System.out.println("基本类型:char 二进制位数:" + Character.SIZE); + System.out.println("包装类:java.lang.Character"); + // 以数值形式而不是字符形式将Character.MIN_VALUE输出到控制台 + System.out.println("最小值:Character.MIN_VALUE="+ (int) Character.MIN_VALUE); + // 以数值形式而不是字符形式将Character.MAX_VALUE输出到控制台 + System.out.println("最大值:Character.MAX_VALUE="+ (int) Character.MAX_VALUE); +} +``` + +字节相关知识: + +> 位(bit):是计算机 内部数据 储存的最小单位,11001100是一个八位二进制数。 +> +> 字节(byte):是计算机中 数据处理 的基本单位,习惯上用大写 B 来表示。 +> +> ​ 1B(byte,字节)= 8bit(位) +> +> 字符:是指计算机中使用的字母、数字、字和符号 + +ASCIIS码: + +| 内容 | 占用大小 | +| ------------------------- | -------- | +| 1个英文字符(不分大小写) | 1个字节 | +| 1个中文汉字 | 2个字节 | +| 1个ASCII码 | 1个字节 | + +UTF-8编码: + +| 内容 | 占用大小 | +| --------------- | -------- | +| 1个英文字符 | 1个字节 | +| 英文标点 | 1个字节 | +| 1个中文(含繁体 | 3个字节 | +| 中文标点 | 3个字节 | + +Unicode编码: + +| 内容 | 占用大小 | +| --------------- | -------- | +| 1个英文字符 | 2个字节 | +| 英文标点 | 2个字节 | +| 1个中文(含繁体 | 2个字节 | +| 中文标点 | 2个字节 | + +1bit表示1位 +1Byte表示一个字节 + +1B=8b +1024B=1KB +1024KB=1M +1024M=1G + +------ + +那有人会问:电脑的32位和64位的区别是什么呢? + +- 32位操作系统只可以使用32位的cpu,而64位的CPU既可以安装32位操作系统也可以安装64位操作 系统。 + +- 寻址能力简单点说就是支持的内存大小能力,64位系统最多可以支达128 GB的内存,而32位系统最 多只可以支持4G内存。 + +- 32位操作系统只可以安装使用32位架构设计的软件,而64位的CPU既可以安装使用32位软件也可以 安装使用64位软件。 + +- 现在的电脑都是64位了! + +回到正题,我们了解了这些知识后,我们自己定义一些变量来看! + +```java +public static void main(String[] args) { + //整型 + int i1=100; + //长整型 + long i2=998877665544332211L; + //短整型 + short i3=235; + //浮点型 + double d1=3.5; //双精度 + double d2=3; + float f1=(float)3.5; //单精度 + float f2=3.5f; //单精度 + //布尔类型 boolean true真/false假 + boolean isPass=true; + boolean isOk=false; + boolean isBig=5>8; + if(isPass){ + System.out.println("通过了"); + }else{ + System.out.println("未通过"); + } + //单字符 + char f='女'; + char m='男'; +} +``` + +Java语言的整型常数默认为int型,浮点数默认是Double + +### 3、整型拓展 + +在我们计算机中存在很多进制问题,十进制,八进制,十六进制等等的问题,他们怎么表示呢? + +- 十进制整数,如:99, -500, 0。 + +- 八进制整数,要求以 0 开头,如:015。 + +- 十六进制数,要求 0x 或 0X 开头,如:0x15 。 + +演示: + +```java +//整型 +int i= 10; +int i2= 010; +int i3= 0x10; +System.out.println(i); //10 +System.out.println(i2); //8 +System.out.println(i3); //16 +``` + +### 4、浮点型拓展 + +【金融面试问:银行金融业务用什么类型表示?】 + +浮点类型float, double的数据不适合在不容许舍入误差的金融计算领域。 + +如果需要进行不产生舍入误差的精确数字计算,需要使用BigDecimal类。 + +```java +public static void main(String[] args) { + float f = 0.1f; + double d = 1.0/10; + System.out.println(f==d); //false + + float d1 = 2131231231f; + float d2 = d1+1; + if(d1==d2){ + System.out.println("d1==d2"); + }else{ + System.out.println("d1!=d2"); + } +} +``` + +最后运行结果: + +>false +>d1==d2 + +**主要理由:** + +由于字长有限,浮点数能够精确表示的数是有限的,因而也是离散的。浮点数一般都存在舍入误差,很 多数字无法精确表示,其结果只能是接近,但不等于;二进制浮点数不能精确的表示0.1,0.01,0.001这样10的负次幂。并不是所有的小数都能可以精确的用二进制浮点数表示。 + +大数值:Java.math下面的两个有用的类:BigInteger和BigDecimal,这两个类可以处理任意长度的数 值。BigInteger实现了任意精度的整数运算。BigDecimal实现了任意精度的浮点运算。 + +**浮点数使用总结:** + +1. 默认是double +2. 浮点数存在舍入误差,很多数字不能精确表示。如果需要进行不产生舍入误差的精确数字计算,需 要使用BigDecimal类。 +3. 避免比较中使用浮点数 + +### 5、字符型拓展 + +单引号用来表示字符常量。例如‘A’是一个字符,它与“A”是不同的,“A”表示一个字符串。 + + char 类型用来表示在Unicode编码表中的字符。 + +Unicode编码被设计用来处理各种语言的所有文字,它占2个字节,可允许有65536个字符; + +科普:2字节=16位,2的16次方=65536,我们用的Excel原来就只有这么多行,并不是无限的 + +【代码演示:字符转int看结果】 + +```java +public static void main(String[] args) { + char c1 = 'a'; + char c2 = '中'; + System.out.println(c1); + System.out.println((int) c1); //97 + System.out.println(c2); + System.out.println((int) c2); //20013 +} +``` + +Unicode具有从0到65535之间的编码,他们通常用从’u0000’到’uFFFF’之间的十六进制值来表示(前缀为 u表示Unicode) + +```java +char c3 = '\u0061'; +System.out.println(c3); //a +``` + +Java 语言中还允许使用转义字符 ‘’ 来将其后的字符转变为其它的含义,有如下常用转义字符: + +![image-20210326234124183](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210326234124183.png) + +【以后我们学的String类,其实是字符序列(char sequence)。在这里给大家一个思考题】 + +```java +//代码1 +String sa=new String("Hello world"); +String sb=new String("Hello world"); +System.out.println(sa==sb); // false +//代码2 +String sc="Hello world"; +String sd="Hello world"; +System.out.println(sc==sd); // true +``` + +大家可以先思考下为什么,之后我们学到对象的时候,会给大家进行内存级别的分析,那时候你会恍然 大悟! + +### 6、布尔型拓展 + +boolean类型(一位,不是一个字节),就是0|1 + +boolean类型有两个值,true和false,不可以 0 或非 0 的整数替代 true 和 false ,这点和C语言不同。 + +boolean 类型用来判断逻辑条件,一般用于程序流程控制。 + +```java +boolean flag = false; +if(flag){ + // true分支 +}else{ + // false分支 +} +``` + +【编码规范:很多新手程序员喜欢这样写】 + +```java +if (is == true && un == false ) {...} +``` + +只有新手才那么写。对于一个熟练的人来说,应该用如下方式来表示: + +```java +if ( is && !un ) {....} +``` + +前面加个 ! 表示否定 + +这点都不难理解吧。所以要习惯去掉所有的==fasle 和 ==true。Less is More!! 代码要精简易读! \ No newline at end of file diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/04.\347\261\273\345\236\213\350\275\254\346\215\242.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/04.\347\261\273\345\236\213\350\275\254\346\215\242.md" new file mode 100644 index 00000000..0af854c1 --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/04.\347\261\273\345\236\213\350\275\254\346\215\242.md" @@ -0,0 +1,159 @@ +--- +title: 类型转换 +date: 2021-04-15 22:28:45 +permalink: /java/se/basic-grammar/type-conversion/ +categories: + - java + - java-se +--- + +## 类型转换 + +由于Java是强类型语言,所以要进行有些运算的时候的,需要用到类型转换。 + +整型、实型(常量)、字符型数据可以混合运算。 + +运算中,不同类型的数据先转化为同一类型,然后进行运算。 + +转换从低级到高级(根据容量来看)。 + +> 低 ------------------------------------> 高 +> +> byte,short,char—> int —> long—> float —> double + +数据类型转换必须满足如下规则: + +- 不能对boolean类型进行类型转换。 + +- 不能把对象类型转换成不相关类的对象。 + +- 在把容量大的类型转换为容量小的类型时必须使用强制类型转换。 + +- 转换过程中可能导致溢出或损失精度,例如: + + ```java + int i = 128; + byte b = (byte)i; + ``` + + 因为 byte 类型是 8 位,最大值为127,所以当 int 强制转换为 byte 类型时,值 128 时候就会导致溢出。 + + 推荐文章:[细谈为什么单字节的整数范围是[-128 ~ 127]](https://blog.csdn.net/lirui1212/article/details/114950520) + +浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入,例如: + +> (int)23.7 == 23; +> (int)-45.89f == -45 + +### 1、自动类型转换 + +自动类型转换:容量小的数据类型可以自动转换为容量大的数据类型。 + +例如: short数据类型的位数为16位,就可以自动转换位数为32的int类型,同样float数据类型的位数为 32,可以自动转换为64位的double类型。 + +```java +public static void main(String[] args) { + char c1 = 'a';//定义一个char类型 + int i1 = c1;//char自动类型转换为int + System.out.println("char自动类型转换为int后的值等于" + i1); + char c2 = 'A';//定义一个char类型 + int i2 = c2 + 1;//char 类型和 int 类型计算 + System.out.println("char类型和int计算后的值等于" + i2); +} +``` + +解析:c1 的值为字符 a ,查 ASCII 码表可知对应的 int 类型值为 97,所以i1=97。 A 对应值为 65,所以 i2=65+1=66。 + +### 2、强制类型转换 + +强制类型转换,又被称为造型,用于显式的转换一个数值的类型。 + +在有可能丢失信息的情况下进行的转换是通过造型来完成的,但可能造成精度降低或溢出。 + +强制类型转换的语法格式:` (type)var` ,运算符 “()” 中的 type 表示将值var想要转换成的目标数据类型。 条件是转换的数据类型必须是兼容的。 + +```java +public static void main(String[] args) { + double x = 3.14; + int nx = (int) x; //值为3 + char c = 'a'; + int d = c + 1; + System.out.println(d); //98 + System.out.println((char) d); //b +} +``` + +当将一种类型强制转换成另一种类型,而又超出了目标类型的表示范围,就会被截断成为一个完全不同 的值,溢出。 + +```java +public static void main(String[] args) { + int x = 300; + byte bx = (byte)x; //值为44 + System.out.println(bx); +} +``` + +### 3、常见错误和问题 + +- 操作比较大的数时,要留意是否溢出,尤其是整数操作时; + + ```java + public static void main(String[] args) { + int money = 1000000000; //10亿 + int years = 20; + int total = money * years; //返回的是负数 + long total1 = money * years; //返回的仍然是负数。默认是int,因此结果会转成int值,再转成long。但是已经发生了数据丢失 + long total2 = money * ((long) years); //先将一个因子变成long,整个表达式发生提升。全部用long来计算。 + System.out.println(total); //-1474836480 + System.out.println(total1); //-1474836480 + System.out.println(total2); //20000000000 + } + ``` + +- L和l 的问题: + + - 不要命名名字为l的变量 + + - long类型使用大写L不要用小写。 + + ```java + public static void main(String[] args) { + int l = 2; + long a = 23451l; + System.out.println(l + 1); //3 + System.out.println(a); //23451 + } + ``` + +### 4、JDK7扩展 + +JDK7新特性: **二进制整数** + +由于我们在开发中也经常使用二进制整数,因此JDK7为我们直接提供了二进制整数的类型。 + +我们只要以:0b开头即可。 + +```java +int a = 0b0101; +``` + +JDK7新特性:**下划线分隔符** + +在实际开发和学习中,如果遇到特别长的数字,读懂它令人头疼!JDK7为我们提供了下划线分隔符,可 以按照自己的习惯进行分割。 + +```java +int b = 1_2234_5678; +``` + +我们很容易就知道这是1亿2234万5678啦! 非常符合国人的习惯! + +```java +public static void main(String[] args) { + int a = 0b0101; + int b = 1_2345_7893; + System.out.println(a); //5 + System.out.println(b); //123457893 +} +``` + +## \ No newline at end of file diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/05.\345\217\230\351\207\217\345\222\214\345\270\270\351\207\217.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/05.\345\217\230\351\207\217\345\222\214\345\270\270\351\207\217.md" new file mode 100644 index 00000000..68b0e9c4 --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/05.\345\217\230\351\207\217\345\222\214\345\270\270\351\207\217.md" @@ -0,0 +1,169 @@ +--- +title: 变量和常量 +date: 2021-04-15 22:28:45 +permalink: /java/se/basic-grammar/variate-constant/ +categories: + - java + - java-se +--- + +## 变量,常量 + +### 1、变量(variable) + +变量是什么:就是可以变化的量! + +我们通过变量来操纵存储空间中的数据,变量就是指代这个存储空间!空间位置是确定的,但是里面放 置什么值不确定! 打个比方: + +这就好像我们家里有一个大衣柜,里面有十分多的小格子,我们给格子上贴上标签,放衣服,放鞋子, 放手表等等,此时我们知道了哪里该放什么,但是,我们并不知道里面到底放的是什么牌子的鞋子,是 衣服还是裤子。那个标签就相当于我们的变量,我们给他起了个名字,但是里面要放什么需要我们自己 去放。 + +Java是一种强类型语言,每个变量都必须声明其类型。 + +Java变量是程序中最基本的存储单元,其要素包括变量名,变量类型和作用域。 + +变量在使用前必须对其声明, 只有在变量声明以后,才能为其分配相应长度的存储单元,声明格式为: + +>数据类型 变量名 = 值; +> +>可以使用逗号隔开来声明多个同类型变量。 + +注意事项: + +- 每个变量都有类型,类型可以是基本类型,也可以是引用类型。 +- 变量名必须是合法的标识符。 +- 变量声明是一条完整的语句,因此每一个声明都必须以分号结束 + +【演示】 + +```java +int a, b, c; // 声明三个int型整数:a、 b、c +int d = 3, e = 4, f = 5; // 声明三个整数并赋予初值 +byte z = 22; // 声明并初始化 z +String s = "runoob"; // 声明并初始化字符串 s +double pi = 3.14159; // 声明了双精度浮点型变量 pi +char x = 'x'; // 声明变量 x 的值是字符 'x'。 +``` + +【编码规范】 + +虽然可以在一行声明多个变量,但是不提倡这个风格,逐一声明每一个变量可以提高程序可读性。 + +### 2、变量作用域 + +变量根据作用域可划分为三种: + +- 类变量(静态变量: static variable):独立于方法之外的变量,用 static 修饰。 +- 实例变量(成员变量:member variable):独立于方法之外的变量,不过没有 static 修饰。 +- 局部变量(lacal variable):类的方法中的变量。 + +```java +public class Variable{ + static int allClicks = 0; // 类变量 + String str = "hello world"; // 实例变量 + + public void method(){ + int i =0; // 局部变量 + } +} +``` + +#### 局部变量 + +方法或语句块内部定义的变量。生命周期是从声明位置开始到 ”}” 为止 + +在使用前必须先声明和初始化(赋初值)。 + +局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。 + +```java +public static void main(String[] args) { + int i; + int j = i + 5; // 编译出错,变量i还未被初始化 + System.out.println(j); +} +``` + +修改为: + +```java +public static void main(String[] args) { + int i=10; + int j = i+5 ; + System.out.println(j); +} +``` + +#### 实例变量 + +方法外部、类的内部定义的变量。 + +从属于对象,生命周期伴随对象始终。 + +如果不自行初始化,他会自动初始化成该类型的默认初始值 + +(数值型变量初始化成0或0.0,字符型变量的初始化值是16位的0,布尔型默认是false) + +```java +public class Test { + // 这个实例变量对子类可见 + public String name; + // 私有变量,仅在该类可见 + private double salary; +} +``` + +#### 静态变量 + +使用static定义。 + +从属于类,生命周期伴随类始终,从类加载到卸载。 + +(注:讲完内存分析后我们再深入!先放一放这个概念!) + +> 不同的类之间需要对同一个变量进行操作,比如一个水池,同时打开入水口和出水口,进水和出水这两个动作会同时影响到池中的水量,此时池中的水量就可以认为是一个共享的变量。该变量就是静态变量 +> +> 静态简单的说是 被类的所有对象共享,比如有一个类,有学校,姓名,年龄三个参数,调用就需要给这三个赋上值,假如这些人都是一个学校的,每次调用都需要赋值就太重复,加上静态的话,一个赋值了,每次调用则都是那个值 + +如果不自行初始化,他会自动初始化成该类型的默认初始值 + +(数值型变量初始化成0或0.0,字符型变量的初始化值是16位的0,布尔型默认是false) + +```java +public class Employee { + //salary是静态的私有变量 + private static double salary; + // DEPARTMENT是一个常量 + public static final String DEPARTMENT = "开发人员"; + public static void main(String[] args){ + salary = 10000; + System.out.println(DEPARTMENT+"平均工资:"+salary); + } +} +``` + +### 3、常量 + +常量(Constant):初始化(initialize)后不能再改变值!不会变动的值。 + +所谓常量可以理解成一种特殊的变量,它的值被设定后,在程序运行过程中不允许被改变。 + +```java +final 常量名=值; +final double PI=3.14; +final String LOVE="hello"; +``` + +常量名一般使用大写字符。 + +程序中使用常量可以提高代码的可维护性。例如,在项目开发时,我们需要指定用户的性别,此时可以 定义一个常量 SEX,赋值为 "男",在需要指定用户性别的地方直接调用此常量即可,避免了由于用户的 不规范赋值导致程序出错的情况。 + +### 4、变量的命名规范 + +1. 所有变量、方法、类名:见名知意 +2. 类成员变量:首字母小写和驼峰原则 : monthSalary +3. 局部变量:首字母小写和驼峰原则 +4. 常量:大写字母和下划线:MAX_VALUE +5. 类名:首字母大写和驼峰原则: Man, GoodMan +6. 方法名:首字母小写和驼峰原则: run(), runRun() + +具体可参考《阿里巴巴Java开发手册》 \ No newline at end of file diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/06.\350\277\220\347\256\227\347\254\246.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/06.\350\277\220\347\256\227\347\254\246.md" new file mode 100644 index 00000000..4bde3cd5 --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/06.\350\277\220\347\256\227\347\254\246.md" @@ -0,0 +1,297 @@ +--- +title: 运算符 +date: 2021-04-15 22:28:45 +permalink: /java/se/basic-grammar/operator/ +categories: + - java + - java-se +--- + +## 运算符 + +运算符operator + +Java 语言支持如下运算符: + +- 算术运算符 +,-,*,/,%,++,-- +- 赋值运算符 = +- 关系运算符 >,<,>=,<=,==,!= instanceof +- 逻辑运算符 &&,||,! +- 位运算符 &,|,^,~ , >>,<<,>>> (了解!!!) +- 条件运算符 ?: +- 扩展赋值运算符 +=,-=,*=,/= + +### 1、二元运算符 + +两个操作数,来看看我们小时候的数学运算; + +```java +public static void main(String[] args) { + int a = 10; + int b = 20; + int c = 25; + int d = 25; + System.out.println("a + b = " + (a + b) ); + System.out.println("a - b = " + (a - b) ); + System.out.println("a * b = " + (a * b) ); + System.out.println("b / a = " + (b / a) ); +} +``` + +**整数运算** + +如果两个操作数有一个为Long, 则结果也为long + +没有long时,结果为int。即使操作数全为shot,byte,结果也是int. + +```java +public static void main(String[] args) { + long a = 1231321311231231L; + int b = 1213; + short c = 10; + byte d = 8; + + System.out.println(a + b + c + d); //Long类型 + System.out.println(b + c + d);//Int类型 + System.out.println(c + d);//Int类型 +} +``` + +**浮点运算** + +如果两个操作数有一个为double, 则结果为double + +只有两个操作数都是float, 则结果才为float + +```java +public static void main(String[] args) { + float a = 3.14565F; + double b = 3.194546464; + float c = 1.3123123F; + System.out.println(a+b); //double类型 + System.out.println(b+c); //double类型 + System.out.println(a+c); //float类型 +} +``` + +**关系运算符** + +返回布尔值! + +![image-20210327143549464](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210327143549464.png) + +可参考 [菜鸟教程](https://www.runoob.com/java/java-operators.html) + +### 2、取模运算 + +就是我们小学的取余; + +> 5%3 余 2 + +其操作数可以为浮点数,一般使用整数。如:5.9%3.9=2.000000004 + +**要点**: + +负数%负数=负数; + +负数%正数=负数; + +正数%负数=正数; + +```java +public static void main(String[] args) { + System.out.println(9 % 4); //1 + System.out.println(-9 % -4); //-1 + System.out.println(-10 % 4); //-2 + System.out.println(9 % -4); //1 +} +``` + +【注:一般都是正整数运算,进行结果的判断!】 + +### 3、一元运算符 + +自增(++)自减(--)运算符是一种特殊的算术运算符,在算术运算符中需要两个操作数来进行运算, 而自增自减运算符是一个操作数,分为前缀和后缀两种。 + +```java +public static void main(String[] args) { + int a = 3; + int b = a++; //执行完后,b=3。先给b赋值,再自增。 + int c = ++a; //执行完后,c=5。先自增,再给b赋值 +} +``` + +注意:java中的乘幂处理 + +```java +public static void main(String[] args) { + int a = 3^2; //java中不能这么处理, ^是异或符号。 + double b = Math.pow(3, 2); +} +``` + +Math类提供了很多科学和工程计算需要的方法和常数。特殊的运算都需要运用到方法! + +### 4、逻辑运算符 + +逻辑与:&&和& +逻辑或:||和| +逻辑非:! + +![image-20210327144148985](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210327144148985.png) + +【演示】 + +```java +public static void main(String[] args) { + boolean a = true; + boolean b = false; + System.out.println("a && b = " + (a&&b)); + System.out.println("a || b = " + (a||b) ); + System.out.println("!(a && b) = " + !(a && b)); +} +``` + +**逻辑与** 和 **逻辑或** 采用短路的方式。从左到右计算,如果确定值则不会再计算下去。在两个操作数都为 true时,结果才为true,但是当得到第一个操作为false时,其结果就必定是false,这时候就不会再判断 第二个操作了。 + +逻辑与只要有一个为false, 则直接返回false. + +逻辑或只要有一个为true, 则直接返回true; + +```java +public static void main(String[] args){ + int a = 5;//定义一个变量; + boolean b = (a<4)&&(a++<10); + System.out.println("使用短路逻辑运算符的结果为"+b); + System.out.println("a的结果为"+a); +} +``` + +解析: 该程序使用到了短路逻辑运算符(&&),首先判断 a<4 的结果为 false,则 b 的结果必定是 false, 所以不再执行第二个操作 a++<10 的判断,所以 a 的值为 5。 + +### 5、位运算符 + +Java定义了位运算符,应用于整数类型(int),长整型(long),短整型(short),字符型(char),和字节型 (byte)等类型。位运算符作用在所有的位上,并且按位运算。 + +假设a = 60,b = 13;它们的二进制格式表示将如下: + +``` +A = 0011 1100 +B = 0000 1101 +----------------- +A&B = 0000 1100 +A | B = 0011 1101 +A ^ B = 0011 0001 +~A= 1100 0011 +``` + +![image-20210327144737549](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210327144737549.png) + +右移一位相当于除2取商。 + +左移一位相当于乘2。 + +【常见面试题:int a=2*8怎样运算效率最快?】 + +```java +public static void main(String[] args) { + System.out.println(2 << 3); +} +``` + +用移位运算 int a=2<<3; +a就是2乘以8 最后结果是16 这是最省内存 最有效率的方法 + +这个方法确实高效率的。我来解释一下: +2的二进制是10 在32位存储器里面是0000 0000 0000 0010 +左移三位后变成 0000 0000 0001 0000 也就是16 + +解释一下,在系统中运算是以二进制的形式进行的。相比来说俩个二进制数相乘运算比移位运算慢一 些。 + +位操作是程序设计中对位模式按位或二进制数的一元和二元操作。 在许多古老的微处理器上, 位运算比加减运算略快, 通常位运算比乘除法运算要快很多。 在现代架构中, 情况并非如此:位运算的运算速度 通常与加法运算相同(仍然快于乘法运算). 详细的需要了解计算机的组成原理! + +### 6、扩展运算符 + +![image-20210327150556669](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210327150556669.png) + +```java +public static void main(String[] args) { + int a=10; + int b=20; + + a+=b; // a = a + b + + System.out.println(a+":"+b); +} +``` + +### 7、字符串连接符 + +“+” 运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串 然后再进行连接。 + +```java +//字符串 +String s1="Hello 中文!"; +String s2=1+""; //转换成String +//int +int c = 12; +System.out.println("c=" + c); +``` + +### 8、三目条件运算符 + +三目条件运算符,语法格式: + +> x ? y : z + +其中x为boolean类型表达式,先计算x的值,若为true,则整个三目运算的结果为表达式y的值,否则整个运算结果为表达式z的值。 + +【演示】 + +```java +public static void main(String[] args) { + int score = 80; + String type = score < 60 ? "不及格" : "及格"; + System.out.println("type= " + type); +} +``` + +三元运算符在真实开发中十分的常见,大家可以多练习使用,之后我们会讲解分支语句,可以利用三元运算符做到更加精简代码!便于理解! + +### 9、运算符优先级 + +我们小学都学过:先加减,后乘除,所以优先级我们并不陌生。 + +当多个运算符出现在一个表达式中,谁先谁后呢?这就涉及到运算符的优先级别的问题。在一个多运算符的表达式中,运算符优先级不同会导致最后得出的结果差别甚大。 + +下表中具有最高优先级的运算符在的表的最上面,最低优先级的在表的底部。 + +| 类别 | 操作符 | 关联性 | +| :------- | :----------------------------------------- | :------- | +| 后缀 | () [] . (点操作符) | 左到右 | +| 一元 | expr++ expr-- | 从左到右 | +| 一元 | ++expr --expr + - ~ ! | 从右到左 | +| 乘性 | * /% | 左到右 | +| 加性 | + - | 左到右 | +| 移位 | >> >>> << | 左到右 | +| 关系 | > >= < <= | 左到右 | +| 相等 | == != | 左到右 | +| 按位与 | & | 左到右 | +| 按位异或 | ^ | 左到右 | +| 按位或 | \| | 左到右 | +| 逻辑与 | && | 左到右 | +| 逻辑或 | \| \| | 左到右 | +| 条件 | ?: | 从右到左 | +| 赋值 | = + = - = * = / =%= >> = << =&= ^ = \| = | 从右到左 | +| 逗号 | , | 左到右 | + +大家不需要去刻意的记住,表达式里面优先使用小括号来组织!!方便理解和使用,不建议写非常冗余 的代码运算! + +```java +public static void main(String[] args) { + boolean flag = 1<4*5&&122>3||'q'+3<5; + System.out.println(flag); +} +``` + +## \ No newline at end of file diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/07.\345\214\205\346\234\272\345\210\266.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/07.\345\214\205\346\234\272\345\210\266.md" new file mode 100644 index 00000000..02057a24 --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/07.\345\214\205\346\234\272\345\210\266.md" @@ -0,0 +1,128 @@ +--- +title: 包机制 +date: 2021-04-15 22:28:45 +permalink: /java/se/basic-grammar/packet/ +categories: + - java + - java-se +--- + +# 包机制 + +## 1、问题发现 + +存在这样一个问题:当定义了多个类的时候,可能会发生类名的重复问题。 + +解决方式:在java中采用包机制处理开发者定义的类名冲突问题。 + +就好比我们平时的用电脑,一个文件夹下不能存在同名的文件,我们要是有这样的需求,但是又不想换 名字,我们就可以考虑使用新建一个文件夹来存放!在我们的Java中也是这样的。 + +我们在idea中创建包,输入代码后,第一行idea默认会有:package 包名路径,例如 + +![image-20210327152247746](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210327152247746.png) + +![image-20210327152310010](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210327152310010.png) + +就要求此份java文件必须保存在这样一个目录下,这样Java解释器才能找到它。 在IDEA中能正确运行, 你可以去Windows下的工程中查看,HelloWorld这个文件必是在这样的目录结构下的。 + +3-6行是文档注释,便于把java文件打包成文档自动生成文件信息,以后会遇到,在阿里巴巴开发手册中,要求添加@author的注释信息 ,可以下载个阿里巴巴开发手册插件配置在ide里 + +## 2、包的作用 + +为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间。 + +**包的作用:** + +1、把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。 + +2、如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。 + +3、包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。 + +Java 使用包(package)这种机制是为了防止命名冲突,访问控制,提供搜索和定位类(class)、接口、枚举(enumerations)和注释(annotation)等。 + +包语句的语法格式为: + +```java +package pkg1[.pkg2[.pkg3…]]; +``` + +例如,一个Something.java 文件它的内容: + +```java +package net.java.util; +public class Something{ +... +} +``` + +那么它的路径应该是 net/java/util/Something.java 这样保存的。 + +package(包) 的作用是把不同的 java 程序分类保存,更方便的被其他 java 程序调用。 + +一个包(package)可以定义为一组相互联系的类型(类、接口、枚举和注释),为这些类型提供访问 保护和命名空间管理的功能。 + +以下是一些 Java 中的包: + +- java.lang-打包基础的类 +- java.io-包含输入输出功能的函数 + +开发者可以自己把一组类和接口等打包,并定义自己的包。而且在实际开发中这样做是值得提倡的,当你自己完成类的实现之后,将相关的类分组,可以让其他的编程者更容易地确定哪些类、接口、枚举和 注释等是相关的。 + +由于包创建了新的命名空间(namespace),所以不会跟其他包中的任何名字产生命名冲突。使用包这 种机制,更容易实现访问控制,并且让定位相关类更加简单。 + +## 3、创建包 + +创建包的时候,你需要为这个包取一个合适的名字。之后,如果其他的一个源文件包含了这个包提供的类、接口、枚举或者注释类型的时候,都必须将这个包的声明放在这个源文件的开头。 + +包声明应该在源文件的第一行,每个源文件只能有一个包声明,这个文件中的每个类型都应用于它。 + +如果一个源文件中没有使用包声明,那么其中的类,函数,枚举,注释等将被放在一个无名的包 (unnamed package)中。 + +一般利用公司域名倒置作为报名; + +例子: + +www.baidu.com 包名:com.baidu.www + +bbs.baidu.com 包名:com.baidu.bbs + +我们平时也可以按照自己的公司域名去写,比如:com.kuangstudy.utils + +## 4、import 关键字 + +为了能够使用某一个包的成员,我们需要在 Java 程序中明确导入该包。使用 "import" 语句可完成此功能。 + +在 java 源文件中 import 语句应位于 package 语句之后,所有类的定义之前,可以没有,也可以有多条,其语法格式为: + +```java +import package1[.package2…].(classname|*); +``` + +如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略。 + +要是要用到其他包下的类,就必须要先导包! + +如果两个类重名,需要导入对应的包,否则就需要写出完整地址: + +```java +com.kuang.dao.Hello hello = new com.kuang.dao.Hello() +``` + +用 **import** 关键字引入,使用通配符 "*" , 导入io包下的所有类! + +```java +import java.io.*; +``` + +【不建议这样使用,因为会全局扫描,影响速度!】 + +使用 **import** 关键字引入指定类: + +```java +import com.kuang.Hello; +``` + +【注意】类文件中可以包含任意数量的 import 声明。import 声明必须在包声明之后,类声明之前。 + +【编码规范:推荐参考阿里巴巴开发手册编程规范】 \ No newline at end of file diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/08.JavaDoc.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/08.JavaDoc.md" new file mode 100644 index 00000000..ad97814c --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/02.\345\237\272\347\241\200\350\257\255\346\263\225/08.JavaDoc.md" @@ -0,0 +1,68 @@ +--- +title: JavaDoc +date: 2021-04-15 22:28:45 +permalink: /java/se/basic-grammar/javadoc/ +categories: + - java + - java-se +--- + +## JavaDoc + +### 1、简介 + +JavaDoc是一种将注释生成HTML文档的技术,生成的HTML文档类似于Java的API,易读且清晰明了。 在简略介绍JavaDoc写法之后,再看一下在Intellij Idea 中如何将代码中的注释生成HTML文档。 + + javadoc是Sun公司提供的一个技术,它从程序源代码中抽取类、方法、成员等注释形成一个和源代码配 套的API帮助文档。也就是说,只要在编写程序时以一套特定的标签作注释,在程序编写完成后,通过 Javadoc就可以同时形成程序的开发文档了。javadoc命令是用来生成自己API文档的,使用方式:使用 命令行在目标文件所在目录输入javadoc +文件名.java。 + +先看一段样例代码: + +```java +/** + * 这是一个Javadoc测试程序 + * + * @author Kuangshen + * @version 1.0 + * @since 1.5 + */ +public class HelloWorld { + public String name; + + /** + * @param name 姓名 + * @return 返回name姓名 + * @throws Exception 无异常抛出 + */ + public String function(String name) throws Exception { + return name; + } + +} +``` + +解释一下: +以 /* 开始,以 / 结束。 +@author 作者名 +@version 版本号 +@since 指明需要最早使用的jdk版本 +@param 参数名 +@return 返回值情况 +@throws 异常抛出情况 + +### 2、命令行生成Doc + +打开cmd + +切换到文件当前目录 `cd /d E:\java\study\package\test` + +输入指令: javadoc HelloWorld.java + +一般会加上`-encoding UTF-8 -charset UTF-8 ` 解决GBK乱码问题,在中间添加编码设置 + +``` +javadoc -encoding UTF-8 -charset UTF-8 HelloWorld.java +``` + +![image-20210327210911496](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-基础语法.assets/image-20210327210911496.png) + +之后会多出一堆文件,打开index.html查看 \ No newline at end of file diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/01.\347\224\250\346\210\267\344\272\244\344\272\222Scanner.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/01.\347\224\250\346\210\267\344\272\244\344\272\222Scanner.md" new file mode 100644 index 00000000..e62a9c4c --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/01.\347\224\250\346\210\267\344\272\244\344\272\222Scanner.md" @@ -0,0 +1,140 @@ +--- +title: Scanner +date: 2021-04-15 22:45:56 +permalink: /java/se/process-control/scanner/ +categories: + - java + - java-se +--- +# JavaSE-流程控制 + +## 用户交互Scanner + +### 1、Scanner对象 + +之前我们学的基本语法中我们并没有实现程序和人的交互,但是Java给我们提供了这样一个工具类,我 们可以获取用户的输入。java.util.Scanner 是 Java5 的新特征,我们可以通过 Scanner 类来获取用户的输入。 + +下面是创建 Scanner 对象的基本语法: + +```java +Scanner s = new Scanner(System.in); +``` + +接下来我们演示一个最简单的数据输入,并通过 Scanner 类的 next() 与 nextLine() 方法获取输入的字符串,在读取前我们一般需要 使用 hasNext() 与 hasNextLine() 判断是否还有输入的数据。 + +### 2、next & nextLine + +```java +public static void main(String[] args) { + //创建一个扫描器对象,用于接收键盘数据 + Scanner scanner = new Scanner(System.in); + //next方式接收字符串 + System.out.println("Next方式接收:"); + //判断用户还有没有输入字符 + if (scanner.hasNext()) { + String str = scanner.next(); + System.out.println("输入内容:" + str); + } + //凡是属于IO流的类如果不关闭会一直占用资源.要养成好习惯用完就关掉.就好像你接水完了要关水龙头一样.很多下载软件或者视频软件如果你不彻底关, 都会自己上传下载从而占用资源, 你就会觉得卡, 这一个道理. + scanner.close(); +} +``` + +测试数据:Hello World! + +结果:只输出了Hello。 + +接下来我们使用另一个方法来接收数据:nextLine() + +```java +public static void main(String[] args) { + Scanner scan = new Scanner(System.in); + // 从键盘接收数据 + // nextLine方式接收字符串 + System.out.println("nextLine方式接收:"); + // 判断是否还有输入 + if (scan.hasNextLine()) { + String str2 = scan.nextLine(); + System.out.println("输入内容:" + str2); + } + scan.close(); +} +``` + +测试数据:Hello World! + +结果:输出了Hello World! + +**两者区别:** + +next(): + +- 一定要读取到有效字符后才可以结束输入。 +- 对输入有效字符之前遇到的空白,next() 方法会自动将其去掉。 +- 只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。 +- next() 不能得到带有空格的字符串。 + +nextLine(): + +- 以Enter为结束符,也就是说 nextLine()方法返回的是输入回车之前的所有字符。 +- 可以获得空白。 + +### 3、其他方法 + +如果要输入 int 或 float 类型的数据,在 Scanner 类中也有支持,但是在输入之前最好先使用 hasNextXxx() 方法进行验证,再使用 nextXxx() 来读取: + +```java +public static void main(String[] args) { + Scanner scan = new Scanner(System.in); + // 从键盘接收数据 + int i = 0; + float f = 0.0f; + System.out.print("输入整数:"); + if (scan.hasNextInt()) { + // 判断输入的是否是整数 + i = scan.nextInt(); + // 接收整数 + System.out.println("整数数据:" + i); + } else { + // 输入错误的信息 + System.out.println("输入的不是整数!"); + } + System.out.print("输入小数:"); + if (scan.hasNextFloat()) { + // 判断输入的是否是小数 + f = scan.nextFloat(); + // 接收小数 + System.out.println("小数数据:" + f); + } else { + // 输入错误的信息 + System.out.println("输入的不是小数!"); + } + scan.close(); +} +``` + +具体Scanner类都有什么方法,可查看其中的源码,`ctrl`+`鼠标左键` 点中idea中的Scanner + +以下实例我们可以输入多个数字,并求其总和与平均数,每输入一个数字用回车确认,通过输入非数字来结束输入,并输出执行结果: + +```java +public static void main(String[] args) { + //扫描器接收键盘数据 + Scanner scan = new Scanner(System.in); + double sum = 0; //和 + int m = 0; //输入了多少个数字 + //通过循环判断是否还有输入,并在里面对每一次进行求和和统计 + while (scan.hasNextDouble()) { + double x = scan.nextDouble(); + m = m + 1; + sum = sum + x; + } + System.out.println(m + "个数的和为" + sum); + System.out.println(m + "个数的平均值是" + (sum / m)); + scan.close(); +} +``` + +可能很多小伙伴到这里就看不懂写的什么东西了!这里我们使用了我们一会要学的流程控制语句,我们 接下来就去学习这些语句的具体作用! + +Java中的流程控制语句可以这样分类:顺序结构,选择结构,循环结构!这三种结构就足够解决所有的 问题了! diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/02.\351\241\272\345\272\217\347\273\223\346\236\204.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/02.\351\241\272\345\272\217\347\273\223\346\236\204.md" new file mode 100644 index 00000000..0c4765e0 --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/02.\351\241\272\345\272\217\347\273\223\346\236\204.md" @@ -0,0 +1,36 @@ +--- +title: 顺序结构 +date: 2021-04-16 14:11:20 +permalink: /java/se/process-control/sequential-structure/ +categories: + - java + - java-se + - 流程控制 +--- +# JavaSE-流程控制 + +## 顺序结构 + +JAVA的基本结构就是顺序结构,除非特别指明,否则就按照顺序一句一句执行。 + +顺序结构是最简单的算法结构。 + +![image-20210327214132289](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-流程控制.assets/image-20210327214132289.png) + +语句与语句之间,框与框之间是按从上到下的顺序进行的,它是由若干个依次执行的处理步骤组成的, 它是任何一个算法都离不开的一种基本算法结构。 + +顺序结构在程序流程图中的体现就是用流程线将程序框自上而地连接起来,按顺序执行算法步骤。 + +```java +public static void main(String[] args) { + System.out.println("Hello1"); + System.out.println("Hello2"); + System.out.println("Hello3"); + System.out.println("Hello4"); + System.out.println("Hello5"); +} +//按照自上而下的顺序执行!依次输出。 +``` + + + diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/03.\351\200\211\346\213\251\347\273\223\346\236\204.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/03.\351\200\211\346\213\251\347\273\223\346\236\204.md" new file mode 100644 index 00000000..d491e3ad --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/03.\351\200\211\346\213\251\347\273\223\346\236\204.md" @@ -0,0 +1,262 @@ +--- +title: 选择结构 +date: 2021-04-15 22:45:56 +permalink: /java/se/process-control/case-structure/ +categories: + - java + - java-se +--- +# JavaSE-流程控制 + + + +## 选择结构 + +### 1、if单选择结构 + +我们很多时候需要去判断一个东西是否可行,然后我们才去执行,这样一个过程在程序中用if语句来表 示: + +![image-20210327214322066](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-流程控制.assets/image-20210327214322066.png) + +```java +if(布尔表达式){ + //如果布尔表达式为true将执行的语句 +} +``` + +意义:if语句对条件表达式进行一次测试,若测试为真,则执行下面的语句,否则跳过该语句。 + +【演示】比如我们来接收一个用户输入,判断输入的是否为Hello字符串: + +```java +public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + //接收用户输入 + System.out.print("请输入内容:"); + String s = scanner.nextLine(); + + if (s.equals("Hello")){ + System.out.println("输入的是:"+s); + } + + System.out.println("End"); + scanner.close(); +} +``` + +equals方法是用来进行字符串的比较的,之后会详解,这里大家只需要知道他是用来比较字符串是否 一致的即可!和==是有区别的。 + +### 2、if双选择结构 + +那现在有个需求,公司要收购一个软件,成功了,给人支付100万元,失败了,自己找人开发。这样的 需求用一个if就搞不定了,我们需要有两个判断,需要一个双选择结构,所以就有了if-else结构。 + +![image-20210327214623634](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-流程控制.assets/image-20210327214623634.png) + +```java +if(布尔表达式){ + //如果布尔表达式的值为true + }else{ + //如果布尔表达式的值为false +} +``` + +意义:当条件表达式为真时,执行语句块1,否则,执行语句块2。也就是else部分。 + +【演示】我们来写一个示例:考试分数大于60就是及格,小于60分就不及格。 + +```java +public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + System.out.print("请输入成绩:"); + int score = scanner.nextInt(); + + if (score>60){ + System.out.println("及格"); + }else { + System.out.println("不及格"); + } + + scanner.close(); +} +``` + +### 3、if多选择结构 + +我们发现上面的示例不符合实际情况,真实的情况还可能存在ABCD,存在区间多级判断。比如90-100 就是A,80-90 就是B.....,在生活中我们很多时候的选择也不仅仅只有两个,所以我们需要一个多选 择结构来处理这类问题! + +![image-20210327214751941](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-流程控制.assets/image-20210327214751941.png) + +```java +if(布尔表达式 1){ + //如果布尔表达式 1的值为true执行代码 +}else if(布尔表达式 2){ + //如果布尔表达式 2的值为true执行代码 +}else if(布尔表达式 3){ + //如果布尔表达式 3的值为true执行代码 +}else { + //如果以上布尔表达式都不为true执行代码 +} +``` + +if 语句后面可以跟 else if…else 语句,这种语句可以检测到多种可能的情况。 + +使用 if,else if,else 语句的时候,需要注意下面几点: + +- if 语句至多有 1 个 else 语句,else 语句在所有的 else if 语句之后。 +- if 语句可以有若干个 else if 语句,它们必须在 else 语句之前。 +- 一旦其中一个 else if 语句检测为 true,其他的 else if 以及 else 语句都将跳过执行。 + +【演示】我们来改造一下上面的成绩案例,学校根据分数区间分为ABCD四个等级! + +```java +public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + System.out.print("请输入成绩:"); + int score = scanner.nextInt(); + + if (score==100){ + System.out.println("恭喜满分"); + }else if (score<100 && score >=90){ + System.out.println("A级"); + }else if (score<90 && score >=80){ + System.out.println("B级"); + }else if (score<80 && score >=70){ + System.out.println("C级"); + }else if (score<70 && score >=60){ + System.out.println("D级"); + }else if (score<60 && score >=0){ + System.out.println("不及格!"); + }else { + System.out.println("成绩输入不合法!"); + } + scanner.close(); +} +``` + +我们平时写程序一定要严谨,不然之后修补Bug是一件十分头疼的事情,要在编写代码的时候就把所有的问题都思考清除,再去一个个解决,这才是一个优秀的程序员应该做的事情,多思考,少犯错! + +### 4、嵌套的if结构 + +使用嵌套的 if…else 语句是合法的。也就是说你可以在另一个 if 或者 else if 语句中使用 if 或者 else if 语 句。你可以像 if 语句一样嵌套 else if...else。 + +```java +if(布尔表达式 1){ + ////如果布尔表达式 1的值为true执行代码 + if(布尔表达式 2){ + ////如果布尔表达式 2的值为true执行代码 + } +} +``` + +有时候我们在解决某些问题的时候,需要缩小查找范围,需要有层级条件判断,提高效率。比如:我们需要寻找一个数,在1-100之间,我们不知道这个数是多少的情况下,我们最笨的方式就是一个个去对比,看他到底是多少,这会花掉你大量的时间,如果可以利用if嵌套比较,我们可以节省大量的成本,如果你有这个思想,你已经很优秀了,因为很多大量的工程师就在寻找能够快速提高,查找和搜索效率的方式。为此提出了一系列的概念,我们生活在大数据时代,我们需要不断的去思考如何提高效率,或许哪一天,你们想出一个算法,能够将分析数据效率提高,或许你就可以在历史的长河中留下一些痕迹了,当然这是后话。 + +【记住一点就好,所有的流程控制语句都可以互相嵌套,互不影响!】 + +### 5、switch多选择结构 + +多选择结构还有一个实现方式就是switch case 语句。 + +switch case 语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支。 + +```java +switch(expression){ + case value : + //语句 + break; //可选 + case value : + //语句 + break; //可选 + //你可以有任意数量的case语句 + default : //可选 + //语句 +} +``` + +switch case 语句有如下规则: + +- switch 语句中的变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。 +- switch 语句可以拥有多个 case 语句。每个 case 后面跟一个要比较的值和冒号。 +- case 语句中的值的数据类型必须与变量的数据类型相同,而且只能是常量或者字面常量。 +- 当变量的值与 case 语句的值相等时,那么 case 语句之后的语句开始执行,直到 break 语句出现才会跳出 switch 语句。 +- 当遇到 break 语句时,switch 语句终止。程序跳转到 switch 语句后面的语句执行。case 语句不必须要包含 break 语句。如果没有 break 语句出现,程序会继续执行下一条 case 语句,直到出现 break 语句。 +- switch 语句可以包含一个 default 分支,该分支一般是 switch 语句的最后一个分支(可以在任何位置,但建议在最后一个)。default 在没有 case 语句的值和变量值相等的时候执行。default 分支不需要 break 语句。 + +switch case 执行时,一定会先进行匹配,匹配成功返回当前 case 的值,再根据是否有 break,判断是否继续输出,或是跳出判断。 + +```java +public static void main(String[] args) { + char grade = 'C'; + switch (grade) { + case 'A': + System.out.println("优秀"); + break; + case 'B': + System.out.println("秀"); + break; + case 'C': + System.out.println("良好"); + break; + case 'D': + System.out.println("及格"); + break; + case 'F': + System.out.println("你需要再努力努力"); + break; + default: + System.out.println("未知等级"); + } + System.out.println("你的等级是 " + grade); +} + +``` + +如果 case 语句块中没有 break 语句时,匹配成功后,从当前 case 开始,后续所有 case 的值都会输 出。如果后续的 case 语句块有 break 语句则会跳出判断。 + +```java +public static void main(String[] args) { + int i = 1; + switch (i) { + case 0: + System.out.println("0"); + case 1: + System.out.println("1"); + case 2: + System.out.println("2"); + case 3: + System.out.println("3"); + break; + default: + System.out.println("default"); + } +} +``` + +输出:1,2,3。 + +【JDK7增加了字符串表达式】 + +case 后边的可以是字符串的表达形式 + +```java +public static void main(String[] args) { + String name = "你好"; + + switch (name) { + //JDK7的新特性,表达式结果可以是字符串!!! + case "你好": + System.out.println("你好"); + break; + case "我好": + System.out.println("我好"); + break; + default: + System.out.println("啥都不好"); + break; + } +} +``` + + + diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/04.\345\276\252\347\216\257\347\273\223\346\236\204.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/04.\345\276\252\347\216\257\347\273\223\346\236\204.md" new file mode 100644 index 00000000..5e0b6e05 --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/04.\345\276\252\347\216\257\347\273\223\346\236\204.md" @@ -0,0 +1,329 @@ +--- +title: 循环结构 +date: 2021-04-15 22:45:56 +permalink: /java/se/process-control/loop-structure +categories: + - java + - java-se +--- +# JavaSE-流程控制 + + + +## 循环结构 + +上面选择结构中,我们始终无法让程序一直跑着,我们每次运行就停止了。我们需要规定一个程序运行多少次,运行多久,等等。所以按照我们编程是为了解决人的问题的思想,我们是不是得需要有一个结构来搞定这个事情!于是循环结构自然的诞生了! + +顺序结构的程序语句只能被执行一次。如果您想要同样的操作执行多次,,就需要使用循环结构。 + +Java中有三种主要的循环结构: + +- while 循环 +- do…while 循环 +- for 循环 + +在Java5中引入了一种主要用于数组的增强型for循环。 + +### 1、while 循环 + +while是最基本的循环,它的结构为: + +```java +while( 布尔表达式 ) { + //循环内容 +} +``` + +![image-20210327220634511](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-流程控制.assets/image-20210327220634511.png) + +【图解】在循环刚开始时,会计算一次“布尔表达式”的值,若条件为真,执行循环体。而对于后来每一 次额外的循环,都会在开始前重新计算一次判断是否为真。直到条件不成立,则循环结束。 + +我们大多数情况是会让循环停止下来的,我们需要一个让表达式失效的方式来结束循环。 + +方式有:循环内部控制,外部设立标志位!等 + +```java +public static void main(String[] args) { + int i = 0; + //i小于100就会一直循环 + while (i<100){ + i++; + System.out.println(i); + } +} +``` + +少部分情况需要循环一直执行,比如服务器的请求响应监听等。 + +```java +public static void main(String[] args) { + while (true){ + //等待客户端连接 + //定时检查 + //...... + } +} +``` + +循环条件一直为true就会造成无限循环【死循环】,我们正常的业务编程中应该尽量避免死循环。会影 响程序性能或者造成程序卡死奔溃! + +【案例:计算1+2+3+…+100=?】 + +```java +public static void main(String[] args) { + int i = 0; + int sum = 0; + while (i <= 100) { + sum = sum+i; + i++; + } + System.out.println("Sum= " + sum); +} +``` + +【高斯的故事】 + +德国大数学家高斯(Gauss):高斯是一对普通夫妇的儿子。他的母亲是一个贫穷石匠的女儿,虽然十分聪明,但却没有接受过教育,近似于文盲。在她成为高斯父亲的第二个妻子之前,她从事女佣工作。他的父亲曾做过园丁,工头,商人的助手和一个小保险公司的评估师。当高斯三岁时便能够纠正他父亲的借债账目的事情, 已经成为一个轶事流传至今。他曾说,他在麦仙翁堆上学会计算。能够在头脑中进行复杂的计算,是上帝赐予他一生的天赋。 + +高斯用很短的时间计算出了小学老师布置的任务:对自然数从1到100的求和.他所使用的方法是:对50 对构造成和101的数列求和(1+100,2+99,3+98……),同时得到结果:5050.这一年,高斯9岁. + +这个故事我们在高中数学中的“等差数列求和”听过,当时我们用的公式求解。 + +![img](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-流程控制.assets/5882b2b7d0a20cf4863461a066094b36acaf992d) + +编程难的不是语言,是算法。数学能让你写的程序运算次数更少,效率更高。 + +如果求1+2+3.....+100万呢,虽然100万次的运算加法,对于计算机不算什么,但我们为了效率可以用数学公式进行求解。 + +有能力的同学可以看下编程程序比赛,考验算法能力的,例如:“传智杯”,“蓝桥杯” + +### 2、do…while 循环 + +对于 while 语句而言,如果不满足条件,则不能进入循环。但有时候我们需要即使不满足条件,也至少 执行一次。 + +do…while 循环和 while 循环相似,不同的是,do…while 循环至少会执行一次。 + +```java +do { + //代码语句 +}while(布尔表达式); +``` + +![image-20210327222224162](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-流程控制.assets/image-20210327222224162.png) + +注意:布尔表达式在循环体的后面,所以语句块在检测布尔表达式之前已经执行了。 如果布尔表达式的值为 true,则语句块一直执行,直到布尔表达式的值为 false。 + +我们用do...while改造一下上面的案例! + +```java +public static void main(String[] args) { + int i = 0; + int sum = 0; + do { + sum = sum+i; + i++; + }while (i <= 100); + System.out.println("Sum= " + sum); +} +``` + +执行结果当然是一样的! + +While和do-While的区别: + +while先判断后执行。dowhile是先执行后判断! + +Do...while总是保证循环体会被至少执行一次!这是他们的主要差别。 + +```java +public static void main(String[] args) { + int a = 0; + while(a<0){ + System.out.println(a); + a++; + } + System.out.println("-----"); + do{ + System.out.println(a); + a++; + } while (a<0); +} +``` + +### 3、For循环 + +虽然所有循环结构都可以用 while 或者 do...while表示,但 Java 提供了另一种语句 —— for 循环,使一些循环结构变得更加简单。 + +for循环语句是支持迭代的一种通用结构,是最有效、最灵活的循环结构。 + +for循环执行的次数是在执行前就确定的。语法格式如下: + +```java +for(初始化; 布尔表达式; 更新) { + //代码语句 +} +``` + +![image-20210327222651145](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-流程控制.assets/image-20210327222651145.png) + +关于 for 循环有以下几点说明: + +- 最先执行初始化步骤。可以声明一种类型,但可初始化一个或多个循环控制变量,也可以是空语句。 +- 然后,检测布尔表达式的值。如果为 true,循环体被执行。如果为false,循环终止,开始执行循环体后面的语句。 +- 执行一次循环后,更新循环控制变量(迭代因子控制循环变量的增减。 +- 再次检测布尔表达式。循环执行上面的过程。 + +【演示:while和for输出】 + +```java +public static void main(String[] args) { + int a = 1; //初始化 + + while(a<=100){ //条件判断 + System.out.println(a); //循环体 + a+=2; //迭代 + } + System.out.println("while循环结束!"); + + for(int i = 1;i<=100;i++){ //初始化//条件判断 //迭代 + System.out.println(i); //循环体 + } + System.out.println("while循环结束!"); +} +``` + +我们发现,for循环在知道循环次数的情况下,简化了代码,提高了可读性。我们平时用到的最多的也是 我们的for循环! + +### 4、练习 + +【练习1:计算0到100之间的奇数和偶数的和】 + +```java +public static void main(String[] args) { + int oddSum = 0; //用来保存奇数的和 + int evenSum = 0; //用来存放偶数的和 + for (int i = 0; i <= 100; i++) { + if (i % 2 != 0) { + oddSum += i; + } else { + evenSum += i; + } + } + System.out.println("奇数的和:" + oddSum); + System.out.println("偶数的和:" + evenSum); +} +``` + +【练习2:用while或for循环输出1-1000之间能被5整除的数,并且每行输出3个】 + +``` +public static void main(String[] args) { + for (int j = 1; j <= 1000; j++) { + if (j % 5 == 0) { + System.out.print(j + "\t"); + } + if (j % (5 * 3) == 0) { + System.out.println(); + } + } +} +``` + +【练习3:打印九九乘法表】 + +``` +1*1=1 +1*2=2 2*2=4 +1*3=3 2*3=6 3*3=9 +1*4=4 2*4=8 3*4=12 4*4=16 +1*5=5 2*5=10 3*5=15 4*5=20 5*5=25 +1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36 +1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49 +1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64 +1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81 +``` + +当然,成功的路不止一条,但是我们要追求最完美的一条,如果你做不到,不妨试试笨办法,依旧可以 完成任务!比如一行行输出,也是可以搞定的。一定要多分析! + +我们使用嵌套for循环就可以很轻松解决这个问题了! + +第一步:我们先打印第一列,这个大家应该都会 + +```java +for (int i = 1; i <= 9; i++) { + System.out.println(1 + "*" + i + "=" + (1 * i)); +} +``` + +第二步:我们把固定的1再用一个循环包起来 + +```java +for (int i = 1; i <= 9 ; i++) { + for (int j = 1; j <= 9; j++) { + System.out.println(i + "*" + j + "=" + (i * j)); + } +} +``` + +第三步:去掉重复项,j<=i + +```java +for (int i = 1; i <= 9 ; i++) { + for (int j = 1; j <= i; j++) { + System.out.println(j + "*" + i + "=" + (i * j)); + } +} +``` + +第四步:调整样式 + +```java +for (int i = 1; i <= 9 ; i++) { + for (int j = 1; j <= i; j++) { + System.out.print(j + "*" + i + "=" + (i * j)+ "\t"); + } + System.out.println(); +} +``` + +通过本练习,大家要体会如何分析问题、如何切入问题!在我们以后写代码的过程中,一定要学会将一 个大问题分解成若干小问题,然后,由易到难,各个击破!这也是我们以后开发项目时的基本思维过程。希望大家好好体会! + +### 5、增强for循环 + +Java5 引入了一种主要用于数组或集合的增强型 for 循环。 + +Java 增强 for 循环语法格式如下: + +```java +for(声明语句 : 表达式) +{ + //代码句子 +} +``` + +声明语句:声明新的局部变量,该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句 块,其值与此时数组元素的值相等。 + +表达式:表达式是要访问的数组名,或者是返回值为数组的方法。 + +【演示:增强for循环遍历输出数组元素】 + +```java +public static void main(String[] args) { + + int [] numbers = {10, 20, 30, 40, 50}; + for(int x : numbers ){ + System.out.print( x ); + System.out.print(","); + } + System.out.print("\n"); + + String [] names ={"James", "Larry", "Tom", "Lacy"}; + for( String name : names ) { + System.out.print( name ); + System.out.print(","); + } +} +``` + +我们现在搞不懂这个没关系,就是拉出来和大家见一面,下章就讲解数组了! + diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/05.break\345\222\214continue.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/05.break\345\222\214continue.md" new file mode 100644 index 00000000..21461d05 --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/03.\346\265\201\347\250\213\346\216\247\345\210\266/05.break\345\222\214continue.md" @@ -0,0 +1,95 @@ +--- +title: break和continue +date: 2021-04-15 22:45:56 +permalink: /java/se/process-control/break-continue +categories: + - java + - java-se +--- +# JavaSE-流程控制 + + + +## break & continue + +### 1、break 关键字 + +break 主要用在循环语句或者 switch 语句中,用来跳出整个语句块。 + +break 跳出最里层的循环,并且继续执行该循环下面的语句。 + +【演示:跳出循环】 + +```java +public static void main(String[] args) { + int i = 0; + while (i < 100) { + i++; + System.out.println(i); + if (i == 30) { + break; + } + } +} +``` + +switch 语句中break在上面已经详细说明了 + +### 2、continue 关键字 + +continue 适用于任何循环控制结构中。作用是让程序立刻跳转到下一次循环的迭代。 + +在 for 循环中,continue 语句使程序立即跳转到更新语句。 + +在 while 或者 do…while 循环中,程序立即跳转到布尔表达式的判断语句。 + +```java +public static void main(String[] args) { + int i = 0; + while (i < 100) { + i++; + if (i % 10 == 0) { + System.out.println(); + continue; + } + System.out.print(i); + } +} +``` + +### 3、两者区别 + +break在任何循环语句的主体部分,均可用break控制循环的流程。break用于强行退出循环,不执行循环中剩余的语句。(break语句也在switch语句中使用) + +continue 语句用在循环语句体中,用于终止某次循环过程,即跳过循环体中尚未执行的语句,接着进行下一次是否执行循环的判定。 + +### 4、带标签的continue + +【了解即可】 + +1. goto关键字很早就在程序设计语言中出现。尽管goto仍是Java的一个保留字,但并未在语言中得到正式使用;Java没有goto。然而,在break和continue这两个关键字的身上,我们仍然能看出一些 goto的影子---带标签的break和continue。 + +2. “标签”是指后面跟一个冒号的标识符,例如:label: + +3. 对Java来说唯一用到标签的地方是在循环语句之前。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环,由于break和continue关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方。 + +4. 带标签的break和continue的例子: + + 【演示:打印101-150之间所有的质数】 + + ```java + public static void main(String[] args) { + int count = 0; + outer: for (int i = 101; i < 150; i ++) { + for (int j = 2; j < i / 2; j++) { + if (i % j == 0){ + continue outer; + } + } + System.out.print(i+ " "); + } + } + ``` + +【看不懂没关系,只是了解一下即可,知道goto这个保留字和标签的写法】 + diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/04.\346\226\271\346\263\225.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/04.\346\226\271\346\263\225.md" new file mode 100644 index 00000000..b188fb21 --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/04.\346\226\271\346\263\225.md" @@ -0,0 +1,307 @@ +--- +title: 方法 +date: 2021-04-15 22:46:09 +permalink: /java/se/method/ +categories: + - java + - java-se +--- +# JavaSE-方法 + +## 1、何谓方法? + +在前面几个章节中我们经常使用到 System.out.println(),那么它是什么呢? + +- println() 是一个方法。 +- System 是系统类。 +- out 是标准输出对象。 + +这句话的用法是调用系统类 System 中的标准输出对象 out 中的方法 println()。 + +**那么什么是方法呢?** + +Java方法是语句的集合,它们在一起执行一个功能。 + +- 方法是解决一类问题的步骤的有序组合 +- 方法包含于类或对象中 +- 方法在程序中被创建,在其他地方被引用 + +设计方法的原则:方法的本意是功能块,就是实现某个功能的语句块的集合。我们设计方法的时候,最 好保持方法的原子性,就是一个方法只完成1个功能,这样利于我们后期的扩展。 + +**方法的优点** + +- 使程序变得更简短而清晰。 +- 有利于程序维护。 +- 可以提高程序开发的效率。 +- 提高了代码的重用性。 + +回顾:方法的命名规则? + +## 2、方法的定义 + +Java的方法类似于其它语言的函数,是一段用来完成特定功能的代码片段,一般情况下,定义一个方法 包含以下语法: + +``` +修饰符 返回值类型 方法名(参数类型 参数名){ + ... + 方法体 + ... + return 返回值; +} +``` + +方法包含一个方法头和一个方法体。下面是一个方法的所有部分: + +- 修饰符 + + 修饰符,这是可选的,告诉编译器如何调用该方法。定义了该方法的访问类型。 + +- 返回值类型 + + 方法可能会返回值。returnValueType 是方法返回值的数据类型。有些方法执行所需 的操作,但没有返回值。在这种情况下,returnValueType 是关键字void。 + +- 方法名 + + 是方法的实际名称。方法名和参数表共同构成方法签名。 + +- 参数类型 + + 参数像是一个占位符。当方法被调用时,传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数。 + + - 形式参数:在方法被调用时用于接收外界输入的数据。 + - 实参:调用方法时实际传给方法的数据。 + + ![image-20210328114438468](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-方法.assets/image-20210328114438468.png) + +- 方法体 + + 方法体包含具体的语句,定义该方法的功能。 + +比如我们写一个比大小的方法: + +【演示】下面的方法包含 2 个参数 num1 和 num2,它返回这两个参数的最大值。 + +```java +/** 返回两个整型变量数据的较大值 */ +public static int max(int num1, int num2) { + int result; + if (num1 > num2){ + result = num1; + } + else{ + result = num2; + } + + return result; +} +``` + +【演示:加法】 + +```java +public int add(int num1, int num2) { + return num1+num2; +} +``` + +## 3、方法调用 + +Java 支持两种调用方法的方式,根据方法是否返回值来选择。 + +当程序调用一个方法时,程序的控制权交给了被调用的方法。当被调用方法的返回语句执行或者到达方法体闭括号时候交还控制权给程序。 + +当方法返回一个值的时候,方法调用通常被当做一个值。例如: + +```java +int larger = max(30, 40); +``` + +Java语言中使用下述形式调用方法:对象名.方法名(实参列表) + +如果方法返回值是void,方法调用一定是一条语句。例如,方法println返回void。下面的调用是个语句: + +```java +System.out.println("Hello,kuangshen!"); +``` + +【演示:定义方法并且调用它】 + +```java +public static void main(String[] args) { + int i = 5; + int j = 2; + int k = max(i, j); + System.out.println( i + " 和 " + j + " 比较,最大值是:" + k); +} + +/** 返回两个整数变量较大的值 */ +public static int max(int num1, int num2) { + int result; + if (num1 > num2){ + result = num1; + } + else{ + result = num2; + } + return result; +} +``` + +这个程序包含 main 方法和 max 方法。main 方法是被 JVM 调用的,除此之外,main 方法和其它方法 没什么区别。**JAVA中只有值传递!** + +main 方法的头部是不变的,如例子所示,带修饰符 public 和 static,返回 void 类型值,方法名字是 main,此外带个一个 String[] 类型参数。String[] 表明参数是字符串数组。 + +## 4、方法的重载 + +上面使用的max方法仅仅适用于int型数据。但如果你想得到两个浮点类型数据的最大值呢? + +解决方法是创建另一个有相同名字但参数不同的方法,如下面代码所示: + +```java +public static double max(double num1, double num2) { + if (num1 > num2) + return num1; + else + return num2; +} + +public static int max(int num1, int num2) { + int result; + if (num1 > num2) + result = num1; + else + result = num2; + return result; +} +``` + +如果你调用max方法时传递的是int型参数,则 int型参数的max方法就会被调用; + +如果传递的是double型参数,则double类型的max方法体会被调用,这叫做方法重载; + +就是说一个类的两个方法拥有相同的名字,但是有不同的参数列表。 + +Java编译器根据方法签名判断哪个方法应该被调用。 + +方法重载可以让程序更清晰易读。执行密切相关任务的方法应该使用相同的名字。 + +重载的方法必须拥有不同的参数列表。你不能仅仅依据修饰符或者返回类型的不同来重载方法。 + +## 5、拓展命令行传参 + +有时候你希望运行一个程序时候再传递给它消息。这要靠传递命令行参数给main()函数实现。 + +命令行参数是在执行程序时候紧跟在程序名字后面的信息。 + +【下面的程序打印所有的命令行参数】 + +```java +public class CommandLine { + public static void main(String args[]){ + for(int i=0; i args[0]: this +> args[1]: is +> args[2]: a +> args[3]: command +> args[4]: line +> args[5]: 200 +> args[6]: -100 + +## 6、可变参数 + +JDK 1.5 开始,Java支持传递同类型的可变参数给一个方法。 + +方法的可变参数的声明如下所示: + +> typeName... parameterName + +在方法声明中,在指定参数类型后加一个省略号(...) + +一个方法中只能指定一个可变参数,它必须是方法的最后一个参数。任何普通的参数必须在它之前声明。 + +```java +public static void main(String[] args) { + // 调用可变参数的方法 + printMax(34, 3, 3, 2, 56.5); + printMax(new double[]{1, 2, 3}); +} + +public static void printMax(double... numbers) { + if (numbers.length == 0) { + System.out.println("No argument passed"); + return; + } + double result = numbers[0]; + //排序! + for (int i = 1; i < numbers.length; i++) { + if (numbers[i] > result) { + result = numbers[i]; + } + } + System.out.println("The max value is " + result); +} +``` + +## 7、递归 + +A方法调用B方法,我们很容易理解! + +递归就是:A方法调用A方法!就是自己调用自己,因此我们在设计递归算法时,一定要指明什么时候自己不调用自己。否则,就是个死循环! + +**递归算法重点:** + +递归是一种常见的解决问题的方法,即把问题逐渐简单化。递归的基本思想就是“自己调用自己”,一个使用递归技术的方法将会直接或者间接的调用自己。 + +利用递归可以用简单的程序来解决一些复杂的问题。它通常把一个大型复杂的问题层层转化为一个与原 问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计 算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。 + +递归结构包括两个部分: + +- 递归头 + + 什么时候不调用自身方法。如果没有头,将陷入死循环。 + +- 递归体 + + 什么时候需要调用自身方法。 + +【演示:利用代码计算5的乘阶!】 + +```java +//5*4*3*2*1 +public static void main(String[] args) { + System.out.println(f(5)); +} +public static int f(int n) { + if (1 == n) + return 1; + else + return n*f(n-1); +} +``` + +![image-20210328140142727](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-方法.assets/image-20210328140142727.png) + +此题中,按照递归的三个条件来分析: + +(1)边界条件:阶乘,乘到最后一个数,即1的时候,返回1,程序执行到底; +(2)递归前进段:当前的参数不等于1的时候,继续调用自身; +(3)递归返回段:从最大的数开始乘,如果当前参数是5,那么就是5* 4,即5 (5-1),即n * (n-1) + +递归其实是方便了程序员难为了机器,递归可以通过数学公式很方便的转换为程序。其优点就是易理 解,容易编程。但递归是用栈机制实现的,每深入一层,都要占去一块栈数据区域,对嵌套层数深的一 些算法,递归会力不从心,空间上会以内存崩溃而告终,而且递归也带来了大量的函数调用,这也有许 多额外的时间开销。所以在深度大时,它的时空性就不好了。(会占用大量的内存空间) + +而迭代虽然效率高,运行时间只因循环次数增加而增加,没什么额外开销,空间上也没有什么增加,但 缺点就是不容易理解,编写复杂问题时困难。 + +能不用递归就不用递归,递归都可以用迭代来代替 \ No newline at end of file diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/05.\346\225\260\347\273\204.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/05.\346\225\260\347\273\204.md" new file mode 100644 index 00000000..664d9f8f --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/05.\346\225\260\347\273\204.md" @@ -0,0 +1,522 @@ +--- +title: +date: 2021-04-15 22:46:20 +permalink: /java/se/array/ +categories: + - java + - java-se +--- +# JavaSE-数组 + +## 数组概述 + +关于数组我们可以把它看作是一个类型的所有数据的一个集合,并用一个数组下标来区分或指定每一个数,例如一个足球队通常会有几十个人,但是我们来认识他们的时候首先会把他们看作是某某对的成员,然后再利用他们的号码来区分每一个队员,这时候,球队就是一个数组,而号码就是数组的下标, 当我们指明是几号队员的时候就找到了这个队员。 同样在编程中,如果我们有一组相同数据类型的数据,例如有10个数字,这时候如果我们要用变量来存放它们的话,就要分别使用10个变量,而且要记住这10个变量的名字,这会十分的麻烦,这时候我们就可以用一个数组变量来存放他们,例如在VB中我们 就可以使用dim a(9) as integer(注意:数组的下标是从0开始的,所以第10个数的话,下标就是 9,a(0)=1)。 使用数组会让程序变的简单,而且避免了定义多个变量的麻烦。 + +数组的定义: + +- 数组是相同类型数据的有序集合 +- 数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成 +- 其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们. + +**数组的四个基本特点:** + +- 其长度是确定的。数组一旦被创建,它的大小就是不可以改变的。 +- 其元素必须是相同类型,不允许出现混合类型。 +- 数组中的元素可以是任何数据类型,包括基本类型和引用类型。 +- 数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中的。 + +## 数组声明创建 + +### 1、声明数组 + +首先必须声明数组变量,才能在程序中使用数组。下面是声明数组变量的语法: + +```java +dataType[] arrayRefVar; // 首选的方法 + +dataType arrayRefVar[]; // 效果相同,但不是首选方法 +``` + +建议使用 dataType[] arrayRefVar 的声明风格声明数组变量。 +dataType arrayRefVar[] 风格是来自 C/C++ 语言 ,在Java中采用是为了让 C/C++ 程序员能够快速理解java语言。 + +```java +double[] myList; // 首选的方法 +double myList[]; // 效果相同,但不是首选方法 +``` + +### 2、创建数组 + +Java语言使用new操作符来创建数组,语法如下: + +```java +arrayRefVar = new dataType[arraySize]; +``` + +上面的语法语句做了两件事: + +- 使用 dataType[arraySize] 创建了一个数组。 +- 把新创建的数组的引用赋值给变量 arrayRefVar。 + +数组变量的声明,和创建数组可以用一条语句完成,如下所示: + +```java +dataType[] arrayRefVar = new dataType[arraySize]; +//例如,创建一个类型ini,大小为10的数组 +int[] myList = new int[10]; +``` + +获取数组长度:arrayRefVar.length + +数组的元素是通过索引访问的。数组索引从 0 开始,所以索引值从 0 到 arrayRefVar.length-1 + +【演示创建一个数组,并赋值,进行访问】 + +```java +public static void main(String[] args) { + //1.声明一个数组 + int[] myList = null; + //2.创建一个数组 + myList = new int[10]; + //3.像数组中存值 + myList[0] = 1; + myList[1] = 2; + myList[2] = 3; + myList[3] = 4; + myList[4] = 5; + myList[5] = 6; + myList[6] = 7; + myList[7] = 8; + myList[8] = 9; + myList[9] = 10; + // 计算所有元素的总和 + double total = 0; + for (int i = 0; i < myList.length; i++) { + total += myList[i]; + } + System.out.println("总和为: " + total); +} +``` + +### 3、内存分析 + +Java内存分析 : + +![image-20210328141316453](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-数组.assets/image-20210328141316453.png) + +1. 声明的时候并没有实例化任何对象,只有在实例化数组对象时,JVM才分配空间,这时才与长度有关。因此,声明数组时不能指定其长度(数组中元素的个数) + + 例如: int a[5]; //非法 + +2. 声明一个数组的时候并没有数组被真正的创建。 + +3. 构造一个数组,必须指定长度 + +```java +//1.声明一个数组 +int[] myList = null; +``` + +![image-20210328141500346](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-数组.assets/image-20210328141500346.png) + +```java +//2.创建一个数组 +myList = new int[10]; +``` + +![image-20210328141525203](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-数组.assets/image-20210328141525203.png) + +```java +//3.像数组中存值 +myList[0] = 1; +myList[1] = 2; +myList[2] = 3; +myList[3] = 4; +myList[4] = 5; +myList[5] = 6; +myList[6] = 7; +myList[7] = 8; +myList[8] = 9; +myList[9] = 10; +``` + +![image-20210328141543324](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-数组.assets/image-20210328141543324.png) + +### 4、三种初始化 + +1、静态初始化 + +除了用new关键字来产生数组以外,还可以直接在定义数组的同时就为数组元素分配空间并赋值。 + +```java +int[] a = {1,2,3}; +Man[] mans = {new Man(1,1),new Man(2,2)}; +``` + +2、动态初始化 + +数组定义、为数组元素分配空间、赋值的操作、分开进行。 + +```java +int[] a = new int[2]; +a[0]=1; +a[1]=2; +``` + +3、数组的默认初始化 + +数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被隐式初始化。 + +```java +public static void main(String[] args) { + int[] a=new int[2]; + boolean[] b = new boolean[2]; + String[] s = new String[2]; + System.out.println(a[0]+":"+a[1]); //0,0 + System.out.println(b[0]+":"+b[1]); //false,false + System.out.println(s[0]+":"+s[1]); //null, null +} +``` + +### 5、数组边界 + +下标的合法区间:[0, length-1],如果越界就会报错; + +```java +public static void main(String[] args) { + int[] a=new int[2]; + System.out.println(a[2]); +} +``` + +> Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2 + +ArrayIndexOutOfBoundsException : 数组下标越界异常! + +### 6、小结 + +数组是相同数据类型(数据类型可以为任意类型)的有序集合 + +数组也是对象。数组元素相当于对象的成员变量(详情请见内存图) + +数组长度的确定的,不可变的。如果越界,则报:ArrayIndexOutofBounds + + + +## 数组使用 + +数组的元素类型和数组的大小都是确定的,所以当处理数组元素时候,我们通常使用基本循环或者 ForEach 循环。 + +【该实例完整地展示了如何创建、初始化和操纵数组】 + +```java +public static void main(String[] args) { + double[] myList = {1.9, 2.9, 3.4, 3.5}; + // 打印所有数组元素 + for (int i = 0; i < myList.length; i++) { + System.out.println(myList[i] + " "); + } + // 计算所有元素的总和 + double total = 0; + for (int i = 0; i < myList.length; i++) { + total += myList[i]; + } + System.out.println("Total is " + total); + // 查找最大元素 + double max = myList[0]; + for (int i = 1; i < myList.length; i++) { + if (myList[i] > max) { + max = myList[i]; + } + } + System.out.println("Max is " + max); + +} +``` + +### 1、For-Each 循环 + +JDK 1.5 引进了一种新的循环类型,被称为 For-Each 循环或者加强型循环,它能在不使用下标的情况下遍历数组。 + +```java +for(type element: array){ + System.out.println(element); +} +``` + +【示例】 + +```java +public static void main(String[] args) { + double[] myList = {1.9, 2.9, 3.4, 3.5}; + + // 打印所有数组元素 + for (double element: myList) { + System.out.println(element); + } +} +``` + +### 2、数组作方法入参 + +数组可以作为参数传递给方法。 + +例如,下面的例子就是一个打印 int 数组中元素的方法 : + +```java +public static void printArray(int[] array) { + for (int i = 0; i < array.length; i++) { + System.out.print(array[i] + " "); + } +} +``` + +### 3、数组作返回值 + +```java +public static int[] reverse(int[] list) { + int[] result = new int[list.length]; + + for (int i = 0, j = result.length - 1; i < list.length; i++, j--) { + result[j] = list[i]; + } + return result; +} +``` + +以上实例中 result 数组作为函数的返回值。 + +## 多维数组 + +多维数组可以看成是数组的数组,比如二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组。 + +**1、多维数组的动态初始化:(以二维数组为例)** + +直接为每一维分配空间,格式如下: + +```java +type[][] typeName = new type[typeLength1][typeLength2]; +``` + + + +type 可以为基本数据类型和复合数据类型,arraylenght1 和 arraylenght2 必须为正整数, arraylenght1 为行数,arraylenght2 为列数。 + +比如定义一个二维数组: + +```java +int a[][] = new int[2][5]; +``` + +解析:二维数组 a 可以看成一个两行三列的数组。 + +**2、多维数组的引用(以二维数组为例)** + +对二维数组中的每个元素,引用方式为 arrayName[index1] [index2],例如: + +> num[1] [0]; + +其实二维甚至多维数组十分好理解,我们把两个或者多个值当做定位就好。 + +原来的数组就是一条线,我们知道一个位置就好 + +二维就是一个面,两点确定一个位置 + +三维呢,就需要三个点来确定 + +**3、获取数组长度:** + +a.length获取的二维数组第一维数组的长度,a[0].length才是获取第二维第一个数组长度。 + +## Arrays 类 + +数组的工具类java.util.Arrays + +由于数组对象本身并没有什么方法可以供我们调用,但API中提供了一个工具类Arrays供我们使用,从 而可以对数据对象进行一些基本的操作。 + +**文档简介:** + +​ ![image-20210328143104527](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-数组.assets/image-20210328143104527.png) + +这个文档,百度即可下载:jdk1.8中文文档 + +Arrays类中的方法都是static修饰的静态方法,在使用的时候可以直接使用类名进行调用,而"不用"使用对象来调用(注意:是"不用" 而不是 "不能") + +java.util.Arrays 类能方便地操作数组. 使用之前需要导包! + +具有以下常用功能: + +- 给数组赋值:通过 fill 方法。 +- 对数组排序:通过 sort 方法,按升序。 +- 比较数组:通过 equals 方法比较数组中元素值是否相等。 +- 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。 + +具体说明请查看下表: + +| 序号 | 方法和说明 | +| :--- | :----------------------------------------------------------- | +| 1 | **public static int binarySearch(Object[] a, Object key)** 用二分查找算法在给定数组中搜索给定值的对象(Byte,Int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引;否则返回 (-(*插入点*) - 1)。 | +| 2 | **public static boolean equals(long[] a, long[] a2)** 如果两个指定的 long 型数组彼此*相等*,则返回 true。如果两个数组包含相同数量的元素,并且两个数组中的所有相应元素对都是相等的,则认为这两个数组是相等的。换句话说,如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。 | +| 3 | **public static void fill(int[] a, int val)** 将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。 | +| 4 | **public static void sort(Object[] a)** 对指定对象数组根据其元素的自然顺序进行升序排列。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。 | + +来源[菜鸟教程](https://www.runoob.com/java/java-array.html),一个编程的基础技术教程网站,适合初学者查看资料,我PHP基础就是在这里学的。 + +### 1、打印数组 + +``` java +public static void main(String[] args) { + int[] a = {1,2}; + System.out.println(a); //[I@1b6d3586 + System.out.println(Arrays.toString(a)); //[1, 2] +} +``` + +### 2、数组排序 + +对指定的 int 型数组按数字升序进行排序 + +```java +public static void main(String[] args) { + int[] a = {1,2,323,23,543,12,59}; + System.out.println(Arrays.toString(a)); + Arrays.sort(a); + System.out.println(Arrays.toString(a)); +} +``` + +### 3、二分法查找 + +在数组中查找指定元素并返回其下标 + +注意:使用二分搜索法来搜索指定的数组,以获得指定的值。必须在进行此调用之前对数组进行排序(通过sort方法等)。如果没有对数组进行排序,则结果是不确定的。 + +如果数组包含多个带有指定值的元素,则无法保证找到的是哪一个。 + +```java +public static void main(String[] args) { + int[] a = {1,2,323,23,543,12,59}; + Arrays.sort(a); //使用二分法查找,必须先对数组进行排序 + System.out.println("该元素的索引:"+Arrays.binarySearch(a, 12)); +} +``` + +### 4、元素填充 + +```java +public static void main(String[] args) { + int[] a = {1,2,323,23,543,12,59}; + Arrays.sort(a); //使用二分法查找,必须先对数组进行排序 + Arrays.fill(a, 2, 4, 100); //将2到4索引的元素替换为100 + System.out.println(Arrays.toString(a));//[1, 2, 100, 100, 59, 323, 543] +} +``` + +### 5、数组转换为List集合 + +```java +int[] a = {3,5,1,9,7}; +List list = Arrays.asList(a); +``` + +我们写代码的时候,可以不用刻意去记住`List` 在idea中,输入`Arrays.asList(a);` 按下alt + 回车键,会自动补全全面的信息,非常方便。 + +学校中教学一般用的是eclipse,别问为啥,问就是这个免费,idea要钱。想用的小伙伴自行百度解决:muscle:,学计算机的人,pojie版的资源找不到就说不过去了。:blush: + +## 常见排序算法 + +### 1、冒泡排序 + +冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。 + +它重复地走访过要排序的元素列,依次比较两个相邻的[元素](https://baike.baidu.com/item/元素/9563223),如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。 + +这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中[二氧化碳](https://baike.baidu.com/item/二氧化碳/349143)的气泡最终会上浮到顶端一样,故名“冒泡排序”。 + +冒泡排序算法的原理如下: + +- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 +- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数 +- 针对所有的元素重复以上的步骤,除了最后一个。 +- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 + + [冒泡排序——《图解算法》](https://blog.csdn.net/zcl_love_wx/article/details/83576962) + +```java +class Bubble { + public int[] sort(int[] array) { + int temp = 0; + // 外层循环,它决定一共走几趟 //-1为了防止溢出 + for (int i = 0; i < array.length - 1; i++) { + int flag = 0; //通过符号位可以减少无谓的比较,如果已经有序了,就退出循环 + //内层循环,它决定每趟走一次 + for (int j = 0; j < array.length - i - 1; j++) { + //如果后一个大于前一个,则换位 + if (array[j + 1] > array[j]) { + temp = array[j]; + array[j] = array[j + 1]; + array[j + 1] = temp; + flag = 1; + } + } + if (flag == 0) { + break; + } + } + return array; + } + + public static void main(String[] args) { + Bubble bubble = new Bubble(); + int[] array = {2, 5, 1, 6, 4, 9, 8, 5, 3, 1, 2, 0}; + int[] sort = bubble.sort(array); + for (int num : sort) { + System.out.print(num + "\t"); + } + } +} +``` + +### 2、选择排序 + +选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中 +选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小 +(大)元素,然后放到排序序列的末尾。以此类推,直到全部待排序的数据元素排完。 选择排序是不稳 +定的排序方法。 + +```java +class SelectSort { + public int[] sort(int arr[]) { + int temp = 0; + for (int i = 0; i < arr.length - 1; i++) { + // 认为目前的数就是最小的, 记录最小数的下标 + int minIndex = i; + for (int j = i + 1; j < arr.length; j++) { + if (arr[minIndex] > arr[j]) {// 修改最小值的下标 + minIndex = j; + } + }// 当退出for就找到这次的最小值,就需要交换位置了 + if (i != minIndex) {//交换当前值和找到的最小值的位置 + temp = arr[i]; + arr[i] = arr[minIndex]; + arr[minIndex] = temp; + } + } + return arr; + } + + public static void main(String[] args) { + SelectSort selectSort = new SelectSort(); + int[] array = {2, 5, 1, 6, 4, 9, 8, 5, 3, 1, 2, 0}; + int[] sort = selectSort.sort(array); + for (int num : sort) { + System.out.print(num + "\t"); + } + } +} +``` + +## 稀疏数组 + +https://blog.csdn.net/baolingye/article/details/99943083 + diff --git "a/docs/01.Java/02.java-\345\237\272\347\241\200/07.\345\274\202\345\270\270\346\234\272\345\210\266.md" "b/docs/01.Java/02.java-\345\237\272\347\241\200/07.\345\274\202\345\270\270\346\234\272\345\210\266.md" new file mode 100644 index 00000000..c4ad0f0c --- /dev/null +++ "b/docs/01.Java/02.java-\345\237\272\347\241\200/07.\345\274\202\345\270\270\346\234\272\345\210\266.md" @@ -0,0 +1,603 @@ +--- +title: 异常机制 +date: 2021-04-15 22:46:39 +permalink: /java/se/exception/ +categories: + - java + - java-se +--- +# JavaSE-异常机制 + +## 异常概念 + +在我们日常生活中,有时会出现各种各样的异常。例如:职工小王开车去上班,在正常情况下,小王会准时到达单位。但是天有不测风云,在小王去上班时,可能会遇到一些异常情况,比如小王的车子出了故障,小王只能改为步行。 + +实际工作中,遇到的情况不可能是非常完美的。比如:你写的某个模块,用户输入不一定符合你的要求、你的程序要打开某个文件,这个文件可能不存在或者文件格式不对,你要读取数据库的数据,数据可能是空的等。我们的程序再跑着,内存或硬盘可能满了。等等。 + +软件程序在运行过程中,非常可能遇到刚刚提到的这些异常问题,我们叫**异常**,英文是:`Exception`, 意思是例外。这些,例外情况,或者叫异常,怎么让我们写的程序做出合理的处理。而不至于程序崩溃。 + +**异常**:指程序运行中出现的不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。 异常发生在程序运行期间,它影响了正常的程序执行流程。 + +比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 `java.lang.Error` ; + +如果你用 `System.out.println(11/0)` ,因为你用0做了除数,会抛出 `java.lang.ArithmeticException` 的异常。 + +异常发生的原因有很多,通常包含以下几大类: + +- 用户输入了非法数据。 +- 要打开的文件不存在。 +- 网络通信时连接中断,或者JVM内存溢出。 + +这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。 + +要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常: + +- **检查性异常**: + + 最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如,要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。 + +- **运行时异常:** + + 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。 + +- **错误:** + + 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出 时,一个错误就发生了,它们在编译也检查不到的。 + +异常指不期而至的各种状况,如:文件找不到、网络连接失败、除0操作、非法参数等。异常是一个 事件,它发生在程序运行期间,干扰了正常的指令流程。 + +Java语言在设计的当初就考虑到这些问题,提出异常处理的框架的方案,所有的异常都可以用一个 异常类 来表示,不同类型的异常对应不同的子类异常(目前我们所说的异常包括错误概念),定义异常处理的规范,在 **JDK1.4** 版本以后增加了异常链机制,从而便于跟踪异常。 + +Java异常是一个描述在代码段中发生异常的对象,当发生异常情况时,一个代表该异常的对象被创建并且在导致该异常的方法中被抛出,而该方法可以选择自己处理异常或者传递该异常。 + +## 异常体系结构 + +Java把异常当作对象来处理,并定义一个基类 `java.lang.Throwable`作为所有异常的超类。 + +在Java API中已经定义了许多异常类,这些异常类分为两大类,错误**Error**和异常**Exception**。 + +Java异常层次结构图: + +![image-20210329151713728](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-异常机制.assets/image-20210329151713728.png) + +从图中可以看出所有异常类型都是内置类 `Throwable` 的子类,因而 `Throwable` 在异常类的层次结构的顶层。 + +接下来 `Throwable` 分成了两个不同的分支,一个分支是`Error`,它表示不希望被程序捕获或者是程序无法处理的错误。另一个分支是`Exception`,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。 + +其中异常类 `Exception` 又分为运行时异常( `RuntimeException` )和非运行时异常。Java异常又可以分为不受检查异常( `Unchecked Exception` )和检查异常( `Checked Exception` )。 + +## 异常之间的区别与联系 + +### 1、Error + +`Error` 类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。 + +比如说: + +Java虚拟机运行错误( `Virtual MachineError` ),当JVM不再有继续执行操作所需的内存资源时, 将出现 `OutOfMemoryError` 。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止; + +还有发生在虚拟机试图执行应用时,如类定义错误( `NoClassDefFoundError` )、链接错误 (`LinkageError `) +这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。 + +对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状 况。在Java中,错误通常是使用 Error 的子类描述。 + +### 2、Exception + +在 `Exception` 分支中有一个重要的子类` RuntimeException` (运行时异常),该类型的异常自动为你所编写的程序定义 `ArrayIndexOutOfBoundsException` (数组下标越界)、 `NullPointerException` (空指针异常)、`ArithmeticException` (算术异常)、 `MissingResourceException` (丢失资源)、 `ClassNotFoundException` (找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处 理,也可以不处理。 + +这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而 `RuntimeException` 之外的异常我们统称为非运行时异常,类型上属于 `Exception` 类及其子类, + +从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 `IOException` 、 `SQLException` 等以及用户自定义的 `Exception` 异常,一般情况下不自定义检查异常。 + +注意: `Error` 和 `Exception` 的区别: `Error` 通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程; `Exception` 通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常。 + +### 3、检查异常和不受检查异常 + +**检查异常**:在正确的程序运行过程中,很容易出现的、情理可容的异常状况,在一定程度上这种异常的发生是可以预测的,并且一旦发生该种异常,就必须采取某种方式进行处理。 + +解析:除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于检查异常,当程序中可能出现这类异常,要么使用try-catch语句进行捕获,要么用throws子句抛出,否则编译无法通过。 + +**不受检查异常**:包括`RuntimeException`及其子类和`Error`。 + +分析: **不受检查异常** 为编译器不要求强制处理的异常, **检查异常** 则是编译器要求必须处置的异常。 + +## Java异常处理机制 + +java异常处理本质:抛出异常和捕获异常 + +### 1、抛出异常 + +要理解抛出异常,首先要明白什么是异常情形(exception condition),它是指阻止当前方法或作用域继续执行的问题。其次把异常情形和普通问题相区分,普通问题是指在当前环境下能得到足够的信息, 总能处理这个错误。 + +对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情。抛出异常后,会有几件事随之发生。 + +首先,是像创建普通的java对象一样将使用 new 在堆上创建一个异常对象;然后,当前的执行路径 (已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序 + +这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要 么换一种方式运行,要么继续运行下去。 + +举例: + +假使我们创建了一个学生对象Student的一个引用stu,在调用的时候可能还没有初始化。所以在使用这个对象引用调用其他方法之前,要先对它进行检查,可以创建一个代表错误信息的对象,并且将它从当前环境中抛出,这样就把错误信息传播到更大的环境中。 + +```java +if(stu == null){ + throw new NullPointerException(); +} +``` + +### 2、捕获异常 + +在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。 + +注意: + +对于 运行时异常 、错误 和 检查异常 ,Java技术所要求的异常处理方式有所不同。 + +由于运行时异常及其子类的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。 + +对于方法运行中可能出现的`Error` ,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数 `Error` 异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。 + +> 对于所有的检查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉检查异常时,它必须声明将抛出异常。 + +### 3、异常处理五个关键字 + +分别是: `try` 、 `catch` 、 `finally` 、 `throw` 、 `throws` + +try :用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。 + +catch : 用于捕获异常。catch用来捕获try语句块中发生的异常。 + +finally :finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络 连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语 句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。 + +throw : 用于抛出异常。 + +throws : 用在方法签名中,用于声明该方法可能抛出的异常。 + +## 处理异常 + +### 1、try -catch + +```java +try{ + //code that might generate exceptions +}catch(Exception e){ + //the code of handling exception1 +}catch(Exception e){ + //the code of handling exception2 +} +``` + +要明白异常捕获,还要理解 监控区域 (guarded region)的概念。它是一段可能产生异常的代码, 并且后面跟着处理这些异常的代码。 + +因而可知,上述 `try-catch` 所描述的即是监控区域,关键词 `try`后的一对大括号将一块可能发生异常的代码包起来,即为监控区域。Java方法在运行过程中发生了异常,则创建异常对象。 + +将异常抛出监控区域之外,由Java运行时系统负责寻找匹配的 catch 子句来捕获异常。若有一个 catch 语句匹配到了,则执行该 catch 块中的异常处理代码,就不再尝试匹配别的 catch 块 了。 + +匹配原则:如果抛出的异常对象属于 catch 子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与 catch 块捕获的异常类型相匹配。 + +【演示】 + +```java +public class TestException { + public static void main(String[] args) { + int a = 1; + int b = 0; + try { // try监控区域 + if (b == 0) throw new ArithmeticException(); // 通过throw语句抛出 + 异常 + System.out.println("a/b的值是:" + a / b); + System.out.println("this will not be printed!"); + } + catch (ArithmeticException e) { // catch捕捉异常 + System.out.println("程序出现异常,变量b不能为0!"); + } + System.out.println("程序正常结束。"); + } +} +``` + +> 输出: +> +> 程序出现异常,变量b不能为0! +> 程序正常结束。 + +注意:显示一个异常的描述, `Throwable` 重载了 `toString( )` 方法(由 Object 定义),所以 它将返回一个包含异常描述的字符串。例如,将前面的 `catch` 块重写成: + +```java +catch (ArithmeticException e) { // catch捕捉异常 + System.out.println("程序出现异常" + e); +} +//输出 +// 程序出现异常java.lang.ArithmeticException +``` + +算术异常属于运行时异常,因而实际上该异常不需要程序抛出,运行时系统自动抛出。如果不用try-catch程序就不会往下执行了。 + +【演示】 + +```java +public class TestException { + public static void main(String[] args) { + int a = 1; + int b = 0; + System.out.println("a/b的值是:" + a / b); + System.out.println("this will not be printed!"); + } +} +/* +结果: +Exception in thread "main" java.lang.ArithmeticException: / by zero +at TestException.main(TestException.java:7) +*/ +``` + +**使用多重的catch语句**:很多情况下,由单个的代码段可能引起多个异常。处理这种情况,我们需要定义两个或者更多的 catch 子句,每个子句捕获一种类型的异常,当异常被引发时,每个 catch 子 句被依次检查,第一个匹配异常类型的子句执行,当一个 catch 子句执行以后,其他的子句将被旁路。 + +编写多重catch语句块注意事项: + +顺序问题:先小后大,即先子类后父类 + +**注意:** + +Java通过异常类描述异常类型。对于有多个 catch 子句的异常程序而言,应该尽量将捕获底层异常类的 catch 子句放在前面,同时尽量将捕获相对高层的异常类的 catch 子句放在后面。否则,捕获 底层异常类的 catch 子句将可能会被屏蔽。 + +**嵌套try语句**: try 语句可以被嵌套。也就是说,一个 try 语句可以在另一个 try 块的内部。每次进入 try 语句,异常的前后关系都会被推入堆栈。如果一个内部的 try 语句不含特殊异常的 catch 处理程序,堆栈将弹出,下一个 try 语句的 catch 处理程序将检查是否与之匹配。这个 过程将继续直到一个 catch 语句被匹配成功,或者是直到所有的嵌套 try 语句被检查完毕。如果没有 catch 语句匹配,Java运行时系统将处理这个异常。 + +【演示】 + +```java +public class NestTry { + public static void main(String[] args) { + try { + int a = args.length; + int b = 42 / a; + System.out.println("a = " + a); + try { + if (a == 1) { + a = a / (a - a); + } + if (a == 2) { + int c[] = {1}; + c[42] = 99; + } + } catch (ArrayIndexOutOfBoundsException e) { + System.out.println("ArrayIndexOutOfBounds :" + e); + } + } catch (ArithmeticException e) { + System.out.println("Divide by 0" + e); + } + } +} +``` + +> 分析运行: +> +> javac NestTry.java +> +> java NestTry one +> a = 1 +> Divide by 0java.lang.ArithmeticException: / by zero +> +> java NestTry one two +> a = 2 +> ArrayIndexOutOfBounds :java.lang.ArrayIndexOutOfBoundsException: 42 + +分析:正如程序中所显示的,该程序在一个try块中嵌套了另一个 try 块。程序工作如下:当你在没有命令行参数的情况下执行该程序,外面的 try 块将产生一个被0除的异常。 + +程序在有一个命令行参数条件下执行,由嵌套的 try 块产生一个被0除的异常,由于内部的 catch 块不匹配这个异常,它将把异常传给外部的 try 块,在外部异常被处理。如果你在具有两个命令行参数的条件下执行该程序,将由内部 try 块产生一个数组边界异常。 + +**注意**:当有方法调用时, try 语句的嵌套可以很隐蔽的发生。例如,我们可以将对方法的调用放在一 个 try 块中。在该方法的内部,有另一个 try 语句。 + +在这种情况下,方法内部的 try 仍然是嵌套在外部调用该方法的 try 块中的。下面我们将对上述例子进行修改,嵌套的 try 块移到方法nesttry()的内部:结果依旧相同! + +```java +public class NestTry { + static void nesttry(int a) { + try { + if (a == 1) { + a = a / (a - a); + } + if (a == 2) { + int c[] = {1}; + c[42] = 99; + } + } catch (ArrayIndexOutOfBoundsException e) { + System.out.println("ArrayIndexOutOfBounds :" + e); + } + } + + public static void main(String[] args) { + try { + int a = args.length; + int b = 42 / a; + System.out.println("a = " + a); + nesttry(a); + } catch (ArithmeticException e) { + System.out.println("Divide by 0" + e); + } + } +} +``` + +### 2、thorw + +到目前为止,我们只是获取了被Java运行时系统引发的异常。然而,我们还可以用`throw`语句抛出明确的异常。 + +语法形式:**throw ThrowableInstance;** + +这里的ThrowableInstance一定是 Throwable 类类型或者 Throwable 子类类型的一个对象。简单的数据类型,例如 int ,char 以及非 Throwable 类,例如 String 或 Object ,不能用作异常。 + +有两种方法可以获取 Throwable 对象:在 catch 子句中使用参数或者使用 new 操作符创建。程序执行完 throw 语句之后立即停止;throw 后面的任何语句不被执行,最邻近的 try 块用来检 查它是否含有一个与异常类型匹配的 catch 语句。 + +如果发现了匹配的块,控制转向该语句;如果没有发现,次包围的 try 块来检查,以此类推。如果没有发现匹配的 catch 块,默认异常处理程序中断程序的执行并且打印堆栈轨迹。 + +```java +class TestThrow { + static void proc() { + try { + throw new NullPointerException("demo"); + } catch (NullPointerException e) { + System.out.println("Caught inside proc"); + throw e; + } + } + + public static void main(String[] args) { + try { + proc(); + } catch (NullPointerException e) { + System.out.println("Recaught: " + e); + } + } +} +``` + +该程序两次处理相同的错误,首先, main() 方法设立了一个异常关系然后调用proc( )。proc( )方法设立了另一个异常处理关系并且立即抛出一个 `NullPointerException` 实例,`NullPointerException` 在 main() 中被再次捕获。 + +该程序阐述了怎样创建Java的标准异常对象,特别注意这一行: + +```java +throw new NullPointerException("demo"); +``` + +分析:此处 new 用来构造一个 `NullPointerException` 实例,所有的Java内置的运行时异常有两个构造方法:一个没有参数,一个带有一个字符串参数。 + +当用第二种形式时,参数指定描述异常的字符串。如果对象用作 print( ) 或者 println( ) 的参数 时,该字符串被显示。这同样可以通过调用getMessage( )来实现,getMessage( )是由 Throwable 定义的。 + +### 3、throws + +如果一个方法可以导致一个异常但不处理它,它必须指定这种行为以使方法的调用者可以保护它们自己而不发生异常。要做到这点,我们可以在方法声明中包含一个 `throws` 子句。 + +一个 throws 子句列举了一个方法可能引发的所有异常类型。这对于除了 Error 或 RuntimeException 及它们子类以外类型的所有异常是必要的。一个方法可以引发的所有其他类型的异常必须在 throws 子句中声明,否则会导致编译错误。 + +```java +public void info() throws Exception +{ + //body of method +} +``` + + Exception 是该方法可能引发的所有的异常,也可以是异常列表,中间以逗号隔开。 + +【例子】 + +```java +class TestThrows{ + static void throw1(){ + System.out.println("Inside throw1 . "); + throw new IllegalAccessException("demo"); + } + + public static void main(String[] args){ + throw1(); + } +} +``` + +上述例子中有两个地方存在错误,你看出来了吗? + +该例子中存在两个错误,首先,throw1( )方法不想处理所导致的异常,因而它必须声明 throws 子句 来列举可能引发的异常即 IllegalAccessException ;其次, main() 方法必须定义 try/catch 语句来捕获该异常。 + +正确例子如下: + +```java +class TestThrows { + static void throw1() throws IllegalAccessException { + System.out.println("Inside throw1 . "); + throw new IllegalAccessException("demo"); + } + + public static void main(String[] args) { + try { + throw1(); + } catch (IllegalAccessException e) { + System.out.println("Caught " + e); + } + } +} +``` + +throws 抛出异常的规则: + +- 如果是不受检查异常( unchecked exception ),即 Error 、 RuntimeException 或它们的子类,那么可以不使用 throws 关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。 +- 必须声明方法可抛出的任何检查异常( checked exception )。即如果一个方法可能出现受可查异常,要么用 try-catch 语句捕获,要么用 throws 子句声明将它抛出,否则会导致编译错误 +- 仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。 +- 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。 + +### 4、finally + +当异常发生时,通常方法的执行将做一个陡峭的非线性的转向,它甚至会过早的导致方法返回。例如, 如果一个方法打开了一个文件并关闭,然后退出,你不希望关闭文件的代码被异常处理机制旁路。 `finally` 关键字为处理这种意外而设计。 + +finally 创建的代码块在 try/catch 块完成之后另一个 try/catch 出现之前执行。 finally 块无论有没有异常抛出都会执行。如果抛出异常,即使没有 catch 子句匹配, finally 也会执行。 + +一个方法将从一个 try/catch 块返回到调用程序的任何时候,经过一个未捕获的异常或者是一个明确的返回语句, finally 子句在方法返回之前仍将执行。这在关闭文件句柄和释放任何在方法开始时被分配的其他资源是很有用。 + +注意: finally 子句是可选项,可以有也可以无,但是每个 try 语句至少需要一个 catch 或 者 finally 子句。 + +【例子】 + +```java +class TestFinally { + static void proc1() { + try { + System.out.println("inside proc1"); + throw new RuntimeException("demo"); + } finally { + System.out.println("proc1's finally"); + } + } + + static void proc2() { + try { + System.out.println("inside proc2"); + return; + } finally { + System.out.println("proc2's finally"); + } + } + + static void proc3() { + try { + System.out.println("inside proc3"); + } finally { + System.out.println("proc3's finally"); + } + } + + public static void main(String[] args) { + try { + proc1(); + } catch (Exception e) { + System.out.println("Exception caught"); + } + proc2(); + proc3(); + } +} +``` + +> 执行结果: +> +> inside proc1 +> proc1's finally +> Exception caught +> inside proc2 +> proc2's finally +> inside proc3 +> proc3's finally + +注:如果 finally 块与一个 try 联合使用, finally 块将在 try 结束之前执行。 + +### 执行顺序 + +**try, catch,finally ,return 执行顺序** + +1. 执行try,catch , 给返回值赋值 +2. 执行finally +3. return + +## 自定义异常 + +使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常。 用户自定义异常类,只需继承 `Exception` 类即可。 + +在程序中使用自定义异常类,大体可分为以下几个步骤: + +- 创建自定义异常类。 +- 在方法中通过 throw 关键字抛出异常对象。 +- 如果在当前抛出异常的方法中处理异常,可以使用 try-catch 语句捕获并处理;否则在方法的声明处通过 throws 关键字指明要抛出给方法调用者的异常,继续进行下一步操作。 +- 在出现异常方法的调用者中捕获并处理异常。 + +【举例】 + +```java +class MyException extends Exception { + private int detail; + MyException(int a){ + detail = a; + } + public String toString(){ + return "MyException ["+ detail + "]"; + } +} +``` + +```java +public class TestMyException { + static void compute(int a) throws MyException { + System.out.println("Called compute(" + a + ")"); + if (a > 10) { + throw new MyException(a); + } + System.out.println("Normal exit!"); + } + + public static void main(String[] args) { + try { + compute(1); + compute(20); + } catch (MyException me) { + System.out.println("Caught " + me); + } + } +} +``` + +> 输出: +> +> Called compute(1) +> Normal exit! +> Called compute(20) +> Caught MyException [20] + +举例二:输入年龄时,年龄不能为负数,否则就报错 + +```java +public class AgeException extends Exception{ + public AgeException() { + } + + public AgeException(String message) { + super(message); + } +} +``` + +```java +public class Demo { + public static void main(String[] args) { + try { + check(-10); + } catch (AgeException e) { + e.printStackTrace(); + } + } + + public static void check(int age) throws AgeException { + if (age < 0) { + throw new AgeException("年龄不能小于0"); + } else { + System.out.println(age); + } + + } +} +``` + +![image-20210329171254831](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-异常机制.assets/image-20210329171254831.png) + +## 总结 + +![image-20210329171311748](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-异常机制.assets/image-20210329171311748.png) + +**实际应用中的经验与总结** + +1. 处理运行时异常时,采用逻辑去合理规避同时辅助try-catch处理 + +2. 在多重catch块后面,可以加一个catch ( Exception )来处理可能会被遗漏的异常 + +3. 对于不确定的代码,也可以加上try-catch,处理潜在的异常 + +4. 尽量去处理异常,切忌只是简单的调用printStackTrace)去打印输出 +5. 具体如何处理异常,要根据不同的业务需求和异常类型去决定 +6. 尽量添加finally语句块去释放占用的资源 + diff --git "a/docs/01.Java/03.java-\351\235\242\345\220\221\345\257\271\350\261\241/06.\351\235\242\345\220\221\345\257\271\350\261\241.md" "b/docs/01.Java/03.java-\351\235\242\345\220\221\345\257\271\350\261\241/06.\351\235\242\345\220\221\345\257\271\350\261\241.md" new file mode 100644 index 00000000..84d3bdae --- /dev/null +++ "b/docs/01.Java/03.java-\351\235\242\345\220\221\345\257\271\350\261\241/06.\351\235\242\345\220\221\345\257\271\350\261\241.md" @@ -0,0 +1,2755 @@ +--- +title: 面向对象 +date: 2021-04-15 22:46:32 +permalink: /java/se/object/ +categories: + - java + - java-se +--- +# JavaSE-面向对象 + +## 面向过程&面向对象 + +语言的进化发展跟生物的进化发展其实是一回事,都是”物以类聚”。相近的感光细胞聚到一起变成了我们的眼睛,相近的嗅觉细胞聚到一起变成了我们的鼻子。 + +语句多了,我们将完成同样功能的相近的语句,聚到了一块儿,便于我们使用。于是,方法出现了! + +变量多了,我们将功能相近的变量组在一起,聚到一起归类,便于我们调用。于是,结构体出现了! + +再后来,方法多了,变量多了!结构体不够用了!我们就将功能相近的变量和方法聚到了一起,于是类和对象出现了! + +寥寥数语,就深刻的展示了语言的进化历史!其实,都非常自然,”物以类聚”。希望大家能记住这句话。 + +企业的发展也是”物以类聚”的过程,完成市场推广的人员聚到一起形成了市场部。完成技术开发的人员聚到一起形成了开发部! + +**面向过程的思维模式** + +面向过程的思维模式是简单的线性思维,思考问题首先陷入第一步做什么、第二步做什么的细节中。这种思维模式适合处理简单的事情,比如:上厕所。 + +如果面对复杂的事情,这种思维模式会陷入令人发疯的状态!比如:如何造神舟十号! + +**面向对象的思维模式** + +面向对象的思维模式说白了就是分类思维模式。思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考。最后,才对某个分类下的细节进行面向过程的思索。 + +这样就可以形成很好的协作分工。比如:设计师分了10个类,然后将10个类交给了10个人分别进行详细设计和编码! + +显然,面向对象适合处理复杂的问题,适合处理需要多人协作的问题! + +如果一个问题需要多人协作一起解决,那么你一定要用面向对象的方式来思考! + +**对于描述复杂的事物,为了从宏观上把握、从整体上合理分析,我们需要使用面向对象的思路来分析整 个系统。但是,具体到微观操作,仍然需要面向过程的思路去处理。** + +## OOP详解 + +### 1、什么是面向对象 + + Java的编程语言是面向对象的,采用这种语言进行编程称为面向对象编程(Object-Oriented Programming, OOP)。 + +面向对象编程的本质就是:以类的方式组织代码,以对象的组织(封装)数据。 + +**抽象(abstract)** + +忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了 +解全部问题,而只是选择其中的一部分,暂时不用关注细节。 + +> 例如:要设计一个学生成绩管理系统,那么对于学生,只关心他的班级、学号、成绩等,而不用去关心他的 +> 身高、体重这些信息。 抽象是什么?就是将多个物体共同点归纳出来,就是抽出像的部分! + +**封装(Encapsulation)** + +封装是面向对象的特征之一,是对象和类概念的主要特性。封装是把过程和数据包围起来,对数据的访 问只能通过指定的方式。 + +在定义一个对象的特性的时候,有必要决定这些特性的可见性,即哪些特性对外部是可见的,哪些特性用于表示内部状态。 + +通常,应禁止直接访问一个对象中数据的实际表示,而应通过操作接口来访问,这称为信息隐藏。 + +信息隐藏是用户对封装性的认识,封装则为信息隐藏提供支持。 + +封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部, 因而可以将应用程序修改带来的影响减少到最低限度。 + +**继承(inheritance)** + +继承是一种联结类的层次模型,并且允许和支持类的重用,它提供了一种明确表述共性的方法 + +新类继承了原始类后,新类就继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。 + +派生类(子类)可以从它的基类(父类)那里继承方法和实例变量,并且派生类(子类)中可以修改或增加新的方法使之更适合特殊的需要继承性很好的解决了软件的可重用性问题。比如说,所有的Windows应用程序都有一个窗口,它们可以看作都是从一个窗口类派生出来的。但是有的应用程序用于文字处理,有的应用程序用于绘图,这是由于派生出了不同的子类,各个子类添加了不同的特性。 + +**多态(polymorphism)** + +多态性是指允许不同类的对象对同一消息作出响应。 + +多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。 + +相同类域的不同对象,调用相同方法,表现出不同的结果 + +**从认识论角度考虑是先有对象后有类。对象,是具体的事物。类,是抽象的,是对对象的抽象。** + +**从代码运行角度考虑是先有类后有对象。类是对象的模板。** + +### 2、类与对象的关系 + +类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物 + +> 例如:我们生活中所说的词语:动物、植物、手机、电脑等等。这些也都是抽象的概念,而不是指的某一个 具体的东西。 + +例如: Person类、Pet类、Car类等,这些类都是用来 描述/定义 某一类具体的事物应该具备的特点和行为 + +对象是抽象概念的具体实例 + +> 例如:张三就是人的一个具体实例,张三家里的旺财就是狗的一个具体实例。能够体现出特点,展现出功能的是具体的实例,而不是一个抽象的概念。 + +【示例】 + +```java +Student s = new Student(1,"tom",20); +s.study(); + +Car c = new Car(1,"BWM",500000); +c.run(); +``` + +对象s就是Student类的一个实例,对象c就是Car类的一个具体实例,能够使用的是具体实例,而不是类。类只是给对象的创建提供了一个参考的模板而已。 + +但是在java中,没有类就没有对象,然而类又是根据具体的功能需求,进行实际的分析,最终抽象出来的。 + +### 3、对象和引用的关系 + +引用 "指向" 对象 + +使用类类型、数组类型、接口类型声明出的变量,都可以指向对象,这种变量就是引用类型变量,简称引用。 + +在程序中,创建出对象后,直接使用并不方便,所以一般会用一个引用类型的变量去接收这个对象,这个就是所说的引用指向对象。 + +总结:对象和引用的关系,就如电视机和遥控器,风筝和线的关系一样。 + +## 方法回顾及加深 + +方法一定是定义在类中的,属于类的成员。 + +### 1、方法的定义 + +> 格式: 修饰符 返回类型 方法名(参数列表)异常抛出类型{...} + +1. **修饰符** + + public、static、abstract、final等等都是修饰符,一个方法可以有多个修饰符。例如程序入口 main方法,就使用了public static这个俩个修饰符 + + 注:如果一个方法或者属性有多个修饰符,这多个修饰符是没有先后顺序的 + +2. **返回类型** + + 方法执行完如果有要返回的数据,那么就要声明返回数据的类型,如果没有返回的数据,那么返回类型就必须写void + + 只有构造方法(构造器)不写任何返回类型也不写void + + 【示例】 + + ```java + public String sayHello(){ + return "hello"; + } + public int max(int a,int b){ + return a>b?a:b; + } + public void print(String msg){ + System.out.println(msg); + } + ``` + + 思考:声明返回类型的方法中一定要出现return语句,那么没有返回类型(void)的方法中,能不能出现 return语句? + + **break和return的区别** + + return 语句的作用 + + > (1) return 从当前的方法中退出,返回到该调用的方法的语句处,继续执行。 + > (2) return 返回一个值给调用该方法的语句,返回值的数据类型必须与方法的声明中的返回值的类型一致。 + > (3) return后面也可以不带参数,不带参数就是返回空,其实主要目的就是用于想中断函数执行,返回 调用函数处。 + + break语句的作用 + + > (1)break在循环体内,强行结束循环的执行,也就是结束整个循环过程,不在判断执行循环的条件是否成立,直接转向循环语句下面的语句。 + > + > (2)当break出现在循环体中的switch语句体内时,其作用只是跳出该switch语句体。 + +3. **方法名** + + 遵守java中标示符的命名规则即可. + +4. 参数列表 + + 根据需求定义,方法可以是无参的,也可以有一个参数,也可以有多个参数 + +5. 异常抛出类型 + + 如果方法中的代码在执行过程中,可能会出现一些异常情况,那么就可以在方法上把这些异常声明并抛出, 也可以同时声明抛出多个异常,使用逗号隔开即可。 + + ```java + public void readFile(String file)throws IOException{ + } + + public void readFile(String file)throws IOException,ClassNotFoundException{ + } + ``` + +### 2、方法调用 + +在类中定义了方法,这个方法中的代码并不会执行,当这个方法被调用的时候,方法中的代码才会被一行一 行顺序执行。 + +1. **非静态方法** + + 没有使用static修饰符修饰的方法,就是非静态方法。 + + 调用这种方法的时候,是"一定"要使用对象的调用。因为非静态方法是属于对象的。(非静态属性也是一样的) + + 【例子】 + + >public class Student{ + > public void say(){} + >} + > + >main: + > + >Student s = new Student(); + > + >s.say(); + +2. **静态方法** + + 使用static修饰符修饰的方法,就是静态方法。 + + 调用这种方法的时候,"可以"使用对象调用,也"可以"使用类来调用,但是推荐使用类进行调用。因为静态方法是属于类的。(静态属性也是一样的) + + 【例子】 + + >public class Student{ + > public static void say(){} + >} + > + > + > + >main: + > + >Student.say(); + +3. **类中方法之间的调用** + + 假设同一个类中有俩个方法,a方法和b方法,a和b都是非静态方法,相互之间可以直接调用。 + + ```java + public void a(){ + b(); + } + public void b(){ + + } + ``` + + a和b都是静态方法,相互之间可以直接调用 + + ```java + public static void a(){ + b(); + } + public static void b(){ + + } + ``` + + a静态方法,b是非静态方法,a方法中不能直接调用b方法,但是b方法中可以直接调用a方法。静态方法不能 调用非静态方法! + + ```java + public static void a(){ + //b();报错 + } + public void b(){ + a(); + } + ``` + + 另外:在同一个类中,静态方法内不能直接访问到类中的非静态属性。 + + 总结:类中方法中的调用,两个方法都是静态或者非静态都可以互相调用,当一个方法是静态,一个方 法是非静态的时候,非静态方法可以调用静态方法,反之不能。 + +### 3、调用方法时的传参 + +**1、形参和实参** + +```java +public static void test(int a){ + +} + +public static void main(String[] args) { + int x = 1; + test(x); +} +``` + +参数列表中的a是方法test的形参(形式上的参数) +调用方法时的x是方法test的实参(实际上的参数) + +注意:形参的名字和实参的名字都只是一个变量的名字,是可以随便写的,我们并不关心这个名字,而是关 心变量的类型以及变量接收的值。 + +**2、值传递和引用传递** + +调用方法进行传参时,分为值传递和引用传递两种。 + +如果参数的类型是基本数据类型,那么就是值传递。 + +如果参数的类型是引用数据类型,那么就是引用传递。 + +值传递是实参把自己变量本身存的简单数值赋值给形参。 +引用传递是实参把自己变量本身存的对象内存地址值赋值给形参。 + +所以值传递和引用传递本质上是一回事,只不过传递的东西的意义不同而已。 + +【示例:值传递】 + +``` +public static void changeNum(int a) { + a = 10; +} + +public static void main(String[] args) { + int a = 1; + System.out.println("before: a = " + a); //1 + changeNum(a); + System.out.println("after: a = " + a); //1 +} +``` + +【示例:引用传递】 + +``` +public class Test { + public static void changeName(Student s) { + s.name = "tom"; + } + + public static void main(String[] args) { + Student s = new Student(); + System.out.println("before: name = " + s.name); //null + changeName(s); + System.out.println("after: name = " + s.name); //tom + } + + static class Student { + String name; + } + +} +``` + +### 4、this关键字 + +在类中,可以使用this关键字表示一些特殊的作用。 + +**1、this在类中的作用** + +区别成员变量和局部变量 + +```java +public class Student{ + private String name; + public void setName(String name){ + //this.name表示类中的属性name + this.name = name; + } +} +``` + +调用类中的其他方法 + +```java +public class Student{ + private String name; + public void setName(String name){ + this.name = name; + } + public void print(){ + //表示调用当前类中的setName方法 + this.setName("tom"); + } +} +``` + +注:默认情况下,setName("tom")和this.setName("tom")的效果是一样的。 + +调用类中的其他构造器 + +```java +public class Student{ + private String name; + public Student(){ + //调用一个参数的构造器,并且参数的类型是String + this("tom"); + } + public Student(String name){ + this.name = name; + } +} +``` + +注:this的这种用法,只能在构造器中使用。普通的方法是不能用的。并且这局调用的代码只能出现在构造器中的第一句。 + +【示例】 + +```java +public class Student{ + private String name; + //编译报错,因为this("tom")不是构造器中的第一句代码. + public Student(){ + System.out.println("hello"); + this("tom"); + } + public Student(String name){ + this.name = name; + } +} +``` + +**2、this关键字在类中的意义** + +this在类中表示当前类将来创建出的对象。 + +【例子】 + +```java +public class Student { + private String name; + + public Student() { + System.out.println("this = " + this); + } + + public static void main(String[] args) { + Student s = new Student(); + System.out.println("s = " + s); + } +} +``` + +运行后看结果可知,this和s打印的结果是一样的,那么其实也就是变量s是从对象的外部执行对象,而this是在对象的内部执行对象本身。 + +这样也就能理解为什么this.name代表的是成员变量,this.setName("tom")代表的是调用成员方法。因为这俩句代码从本质上讲,和在对象外部使用变量s来调用是一样的,s.name和s.setName("tom") + +【this和s打印出来的内存地址是一样的,使用==比较的结果为true。】 + +```java +public class Student{ + public Student getStudent(){ + return this; + } + public static void main(String[] args) { + Student s1 = new Student(); + Student s2 = s1.getStudent(); + System.out.println(s1 == s2);//true + } +} +``` + +【调用类中的this,s1和s2不相等】 + +```java +public class Student{ + private String name; + public void test(){ + System.out.println(this); + } + public static void main(String[] args) { + Student s1 = new Student(); + Student s2 = new Student(); + s1.test(); + s2.test(); + } +} +``` + +注:这句话是要这么来描述的,s1对象中的this和s1相等,s2对象中的this和s2相等,因为类是模板,模板中写的this并不是只有一个,每个对象中都有一个属于自己的this,就是每个对象中都一个属于自己的name属性一样。 + +## 创建与初始化对象 + +**使用new关键字创建对象** + +使用new关键字创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化,以及对类中构造器的调用。 + +那么对main方法中的以下代码:Student s = new Student(); + +1) 为对象分配内存空间,将对象的实例变量自动初始化默认值为0/false/null。(实例变量的隐式赋值) + +2) 如果代码中实例变量有显式赋值,那么就将之前的默认值覆盖掉。(之后可以通过例子看到这个现象) + + 例如:显式赋值,private String name = "tom"; + +3) 调用构造器 + +4) 把对象内存地址值赋值给变量。(=号赋值操作) + +## 构造器 + +类中的构造器也称为构造方法,是在进行创建对象的时候必须要调用的。并且构造器有以下俩个特点: + +1. 必须和类的名字相同 +2. 必须没有返回类型,也不能写void + +**构造器的作用:** + +1. 使用new创建对象的时候必须使用类的构造器 +2. 构造器中的代码执行后,可以给对象中的属性初始化赋值 + +【演示】 + +```java +public class Student{ + private String name; + + public Student(){ + name = "tom"; + } +} +``` + +**构造器重载** + +除了无参构造器之外,很多时候我们还会使用有参构造器,在创建对象时候可以给属性赋值。 + +【例子】 + +```java +public class Student{ + private String name; + public Student(){ + name = "tom"; + } + public Student(String name){ + this.name = name; + } +} +``` + +**构造器之间的调用** + +使用this关键字,在一个构造器中可以调用另一个构造器的代码。 + +注意:this的这种用法不会产生新的对象,只是调用了构造器中的代码而已。一般情况下只有使用new关键字才会创建新对象。 + +【演示】 + +```java +public class Student{ + private String name; + public Student(){ + this(); + } + public Student(String name){ + this.name = name; + } +} +``` + +**默认构造器** + +在java中,即使我们在编写类的时候没有写构造器,那么在编译之后也会自动的添加一个无参构造器,这个无参构造器也被称为默认的构造器。 + +【示例】 + +```java +public class Student{ + +} + +main: +//编译通过,因为有无参构造器 +Student s = new Student(); +``` + +但是,如果我们手动的编写了一个构造器,那么编译后就不会添加任何构造器了 + +【示例】 + +```java +public class Student{ + private String name; + public Student(String name){ + this.name = name; + } +} + +main: +//编译报错,因为没有无参构造器 +Student s = new Student(); +``` + + + +## 内存分析 + +JAVA程序运行的内存分析 + +**栈 stack:** + +1. 每个线程私有,不能实现线程间的共享! +2. 局部变量放置于栈中。 +3. 栈是由系统自动分配,速度快!栈是一个连续的内存空间! + +**堆 heap:** + +1. 放置new出来的对象! +2. 堆是一个不连续的内存空间,分配灵活,速度慢! + +**方法区(也是堆):** + +1. 被所有线程共享! +2. 用来存放程序中永远是不变或唯一的内容。(类代码信息、静态变量、字符串常量) + +![image-20210328212049006](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-面向对象.assets/image-20210328212049006.png) + +注意:本次内存分析,我们的主要目的是让大家了解基本的内存概念。类加载器、Class对象这些更加详细的内容,我们将在后面专门讲反射的课程里面讲。 + +**引用类型的概念** + +1. java中,除了基本数据类型之外的其他类型称之为引用类型。 +2. java中的对象是通过引用来操作的。(引用:reference)说白了,引用指的就是对象的地址! + +**属性(field,或者叫成员变量)** + +1. 属性用于定义该类或该类对象包含的数据或者说静态属性。 + +2. 属性作用范围是整个类体。 + +3. 属性的默认初始化: + + 在定义成员变量时可以对其初始化,如果不对其初始化,Java使用默认的值对其初始化(数值:0,0.0 char:u0000, boolean:false, 所有引用类型:null) + +4. 属性定义格式: + + > [修饰符] 属性类型 属性名 = [默认值] + +**类的方法** + +方法是类和对象动态行为特征的抽象。方法很类似于面向过程中的函数。面向过程中,函数是最基本单位,整个程序有一个个函数调用组成;面向对象中,整个程序的基本单位是类,方法是从属于类或对象的。 + +方法定义格式: + +```java +[修饰符] 方法返回值类型 方法名(形参列表) { + // n条语句 +} +``` + +**java对象的创建和使用** + +- 必须使用 new 关键字创建对象。 + + Person person= new Person (); + +- 使用对象(引用).成员变量来引用对象的成员变量。 + + person.age + +- 使用对象(引用). 方法(参数列表)来调用对象的方法 + + person.setAge(18) + +类中就是:静态的数据 动态的行为 + +学习完类与对象终于认识到什么是类,什么是对象了。 + +接下来要看的就是java的三大特征:继承、封 装、多态。 + +## 封装 + +我要看电视,只需要按一下开关和换台就可以了。有必要了解电视机内部的结构吗?有必要碰碰显像管吗? + +制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只给我们暴露简单的接口,比如: 电源开关。需要让用户知道的暴露出来,不需要让用户了解的全部隐藏起来。这就是封装。 + +白话:该露的露,该藏的藏 + +专业:我们程序设计要追求“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用。 + +**封装(数据的隐藏)** + +在定义一个对象的特性的时候,有必要决定这些特性的可见性,即哪些特性对外部是可见的,哪些特性用于表示内部状态。 + +通常,应禁止直接访问一个对象中数据的实际表示,而应通过操作接口来访问,这称为信息隐藏。 + +### 1、封装的步骤 + +1. 使用private 修饰需要封装的成员变量。 + +2. 提供一个公开的方法设置或者访问私有的属性 + + 设置 通过set方法,命名格式: set属性名(); 属性的首字母要大写 + + 访问 通过get方法,命名格式: get属性名(); 属性的首字母要大写 + +【演示】 + +```java +//对象能在类的外部"直接"访问 +public class Student{ + public String name; + public void println(){ + System.out.println(this.name); + } +} +public class Test{ + public static void main(String[] args){ + Student s = new Student(); + s.name = "tom"; + } +} +``` + +在类中一般不会把数据直接暴露在外部的,而使用private(私有)关键字把数据隐藏起来 + +【演示】 + +```java +public class Student{ + private String name; +} +public class Test{ + public static void main(String[] args){ + Student s = new Student(); + //编译报错,在类的外部不能直接访问类中的私有成员 + s.name = "tom"; + } +} +``` + +如果在类的外部需要访问这些私有属性,那么可以在类中提供对于的get和set方法,以便让用户在类的外部 可以间接的访问到私有属性 + +【示例】 + +```java +//set负责给属性赋值 +//get负责返回属性的值 +public class Student{ + private String name; + public void setName(String name){ + this.name = name; + } + public String getName(){ + return this.name; + } +} +public class Test{ + public static void main(String[] args){ + Student s = new Student(); + s.setName("tom"); + System.out.println(s.getName()); + } +} + +``` + +### 2、作用和意义 + +1. 提高程序的安全性,保护数据。 +2. 隐藏代码的实现细节 +3. 统一用户的调用接口 +4. 提高系统的可维护性 +5. 便于调用者调用。 + +良好的封装,便于修改内部代码,提高可维护性。 + +良好的封装,可进行数据完整性检测,保证数据的有效性。 + +### 3、方法重载 + +类中有多个方法,有着相同的方法名,但是方法的参数各不相同,这种情况被称为方法的重载。方法的重载可以提供方法调用的灵活性。 + +思考:HelloWorld中的System.out.println()方法,为什么可以把不同类型的参数传给这个方法? + +【演示:查看println方法的重载】 idea中`ctrl`+`左键`点击`println` + +例如: + +```java +public class Test{ + public void test(String str){ + + } + public void test(int a){ + + } +} +``` + +**方法重载必须满足以下条件** + +1. 方法名必须相同 + +2. 参数列表必须不同(参数的类型、个数、顺序的不同) + + ```java + public void test(Strig str){} + public void test(int a){} + + public void test(Strig str,double d){} + public void test(Strig str){} + + public void test(Strig str,double d){} + public void test(double d,Strig str){} + ``` + +3. 方法的返回值可以不同,也可以相同。 + +**在java中,判断一个类中的俩个方法是否相同,主要参考俩个方面:方法名字和参数列表** + +## 继承 + +继承:extands + +现实世界中的继承无处不在。比如: + +动物:哺乳动物、爬行动物 + +哺乳动物:灵长目、鲸目等。 + +**继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模。** + +**为什么需要继承?继承的作用?** + +第一好处:继承的本质在于抽象。类是对对象的抽象,继承是对某一批类的抽象。 + +第二好处:为了提高代码的复用性。 + +extands的意思是“扩展”。子类是父类的扩展。 + +【注】JAVA中类只有单继承,没有多继承! 接口可以多继承! + +### 1、继承 + +1. 继承是类和类之间的一种关系。除此之外,类和类之间的关系还有依赖、组合、聚合等。 + +2. 继承关系的俩个类,一个为子类(派生类),一个为父类(基类)。子类继承父类,使用关键字extends来表示。 + + ```java + public class student extends Person{ + } + ``` + +3. 子类和父类之间,从意义上讲应该具有"is a"的关系。 + + > student is a person + > + > dog is a animal + +4. 类和类之间的继承是单继承 + + 一个子类只能"直接"继承一个父类,就像是一个人只能有一个亲生父亲 + 一个父类可以被多子类继承,就像一个父亲可以有多个孩子 + + 注:java中接口和接口之间,有可以继承,并且是多继承。 + +5. 父类中的属性和方法可以被子类继承 + + 子类中继承了父类中的属性和方法后,在子类中能不能直接使用这些属性和方法,是和这些属性和方法原有 的修饰符(public protected default private)相关的。 + + 例如: + + 父类中的属性和方法使用public修饰,在子类中继承后"可以直接"使用 + 父类中的属性和方法使用private修饰,在子类中继承后"不可以直接"使用 + + **注:具体细则在修饰符部分详细说明** + + 父类中的构造器是不能被子类继承的,但是子类的构造器中,会隐式的调用父类中的无参构造器(默认使用 super关键字)。 + + **注:具体细节在super关键字部分详细说明** + +### 2、Object类 + + java中的每一个类都是"直接" 或者 "间接"的继承了Object类。所以每一个对象都和Object类有"is a"的关系。从API文档中,可以看到任何一个类最上层的父类都是Object。(Object类本身除外)AnyClass is a Object。 + +> System.out.println(任何对象 instanceof Object); +> +> 输出结果:true +> 注:任何对象也包含数组对象 + +例如: + +```java +//编译后,Person类会默认继承Object +public class Person{} + +//Student是间接的继承了Object +public class Student extends Person{} +``` + +在Object类中,提供了一些方法被子类继承,那么就意味着,在java中,任何一个对象都可以调用这些被继承过来的方法。(因为Object是所以类的父类) + +例如:toString方法、equals方法、getClass方法等 + +### 3、Super关键字 + +子类继承父类之后,在子类中可以使用this来表示访问或调用子类中的属性或方法,使用super就表示访问或调用父类中的属性和方法。 + +**1、super的使用** + +【访问父类中的属性】 + +```java +public class Person{ + protected String name = "zs"; +} +``` + +```java +public class Student extends Person{ + private String name = "lisi"; + public void tes(String name){ + System.out.println(name); + System.out.println(this.name); + System.out.println(super.name); + } +} +``` + +【调用父类中的方法】 + +```java +public class Person{ + public void print(){ + System.out.println("Person"); + } +} + +``` + +```java +public class Student extends Person{ + public void print(){ + System.out.println("Student"); + } + public void test(){ + print(); + this.print(); + super.print(); + } +} +``` + +【调用父类中的构造器】 + +```java +public class Person{ +} + +public class Student extends Person{ + //编译通过,子类构造器中会隐式的调用父类的无参构造器 + //super(); + public Student(){ + } +} +``` + +父类没有无参构造 + +```java +public class Person{ + protected String name; + public Person(String name){ + this.name = name; + } +} +public class Student extends Person{ + //编译报错,子类构造器中会隐式的调用父类的无参构造器,但是父类中没有无参构造器 + //super(); + public Student(){ + + } +} +``` + +【显式的调用父类的有参构造器】 + +```java +public class Person{ + protected String name; + public Person(String name){ + this.name = name; + } +} + +public class Student extends Person{ + //编译通过,子类构造器中显式的调用父类的有参构造器 + public Student(){ + super("tom"); + } +} +``` + +注:不管是显式还是隐式的父类的构造器,super语句一定要出现在子类构造器中第一行代码。所以this和 super不可能同时使用它们调用构造器的功能,因为它们都要出现在第一行代码位置。 + +【例子】 + +```java +public class Person{ + protected String name; + public Person(String name){ + this.name = name; + } +} + +public class Student extends Person{ + //编译报错,super调用构造器的语句不是第一行代码 + public Student(){ + System.out.println("Student"); + super("tom"); + } +} +``` + +【例子】 + +```java +public class Person{ + protected String name; + public Person(String name){ + this.name = name; + } +} +//编译通过 +public class Student extends Person{ + private int age; + public Student(){ + this(20); + } + public Student(int age){ + super("tom"); + this.age = age; + } +} +``` + +**super使用的注意的地方** + +- 用super调用父类构造方法,必须是构造方法中的第一个语句。 +- super只能出现在子类的方法或者构造方法中 +- super 和 this 不能够同时调用构造方法。(因为this也是在构造方法的第一个语句) + +**super 和 this 的区别** + +1. 代表的事物不一样: + + this:代表所属方法的调用者对象。 + + super:代表父类对象的引用空间。 + +2. 使用前提不一致: + + this:在非继承的条件下也可以使用。 + + super:只能在继承的条件下才能使用。 + +3. 调用构造方法: + + this:调用本类的构造方法。 + + super:调用的父类的构造方法 + + + +### 4、方法重写 + +方法的重写(override) + +- 方法重写只存在于子类和父类(包括直接父类和间接父类)之间。在同一个类中方法只能被重载,不能被重写 +- 静态方法不能重写 + 1. 父类的静态方法不能被子类重写为非静态方法 //编译出错 + 2. 父类的非静态方法不能被子类重写为静态方法;//编译出错 + 3. 子类可以定义与父类的静态方法同名的静态方法(但是这个不是覆盖) + +【例子】 + +> A类继承B类 A和B中都一个相同的静态方法test +> +> B a = new A(); +> a.test();//调用到的是B类中的静态方法test +> +> A a = new A(); +> a.test();//调用到的是A类中的静态方法test +> +> 可以看出静态方法的调用只和变量声明的类型相关 +> 这个和非静态方法的重写之后的效果完全不同 + +私有方法不能被子类重写,子类继承父类后,是不能直接访问父类中的私有方法的,那么就更谈不上重写了 + +```java +public class Person{ + private void run(){} + +} + +//编译通过,但这不是重写,只是俩个类中分别有自己的私有方法 +public class Student extends Person{ + private void run(){} +} +``` + +**重写的语法** + +1. 方法名必须相同 + +2. 参数列表必须相同 + +3. 访问控制修饰符可以被扩大,但是不能被缩小: public protected default private + +4. 抛出异常类型的范围可以被缩小,但是不能被扩大 + + ClassNotFoundException ---> Exception(不能扩大) + +5. 返回类型可以相同,也可以不同。 + + 如果不同的话,子类重写后的方法返回类型必须是父类方法返回类型的子类型。 + + **例如**:父类方法的返回类型是Person,子类重写后的返回类可以是Person也可以是Person的子类型 + +**注**:一般情况下,重写的方法会和父类中的方法的声明完全保持一致,只有方法的实现不同。(也就是大括号中代码不一样) + +```java +public class Person{ + public void run(){} + protected Object test()throws Exception{ + return null; + } +} + +//编译通过,子类继承父类,重写了run和test方法. +public class Student extends Person{ + public void run(){} + public String test(){ + return ""; + } +} +``` + +为什么要重写? + +子类继承父类,继承了父类中的方法,但是父类中的方法并不一定能满足子类中的功能需要,所以子类中需要把方法进行重写。 + +**总结:** + +1. 方法重写的时候,必须存在继承关系。 +2. 方法重写的时候,方法名和形式参数必须跟父类是一致的。 +3. 方法重写的时候,子类的权限修饰符必须要大于或者等于父类的权限修饰符。( private < protected < public,friendly < public ) +4. 方法重写的时候,子类的返回值类型必须小于或者等于父类的返回值类型。( 子类 < 父类 ) 数据类型没有明确的上下级关系 +5. 方法重写的时候,子类的异常类型要小于或者等于父类的异常类型。 + +## 多态 + +### 1、认识多态 + +多态性是OOP中的一个重要特性,主要是用来实现动态联编的,换句话说,就是程序的最终状态只有在执行过程中才被决定而非在编译期间就决定了。这对于大型系统来说能提高系统的灵活性和扩展性。 + +多态可以让我们不用关心某个对象到底是什么具体类型,就可以使用该对象的某些方法,从而实现更加灵活的编程,提高系统的可扩展性。 + +允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。 + +相同类域的不同对象,调用相同的方法,执行结果是不同的 + +1. 一个对象的实际类型是确定的 + + 例如: new Student(); new Person();等 + +2. 可以指向对象的引用的类型有很多 + + 一个对象的实现类型虽然是确定的,但是这个对象所属的类型可能有很多种。 + + 例如:Student继承了Person类 + + ```java + Student s1 = new Student(); + Person s2 = new Student(); + Object s3 = new Student(); + ``` + + 因为Person和Object都是Student的父类型 + + ![image-20210328222401861](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-面向对象.assets/image-20210328222401861.png) + +注:一个对象的实际类型是确定,但是可以指向这个对象的引用的类型,却是可以是这对象实际类型的任意父类型。 + +**一个父类引用可以指向它的任何一个子类对象** + +例如: + +```java +Object o = new AnyClass(); +Person p = null; +p = new Student(); +p = new Teacher(); +p = new Person(); +``` + +**多态中的方法调用** + +```java +public class Person{ + public void run(){} +} + +public class Student extends Person{ + +} +``` + +调用到的run方法,是Student从Person继承过来的run方法 + +```java +Person p = new Student(); +p.run(); +``` + +例如: + +```java +public class Person{ + public void run(){} +} + +public class Student extends Person{ + public void run(){ + //重写run方法 + } +} + +//调用到的run方法,是Student中重写的run方法 +public static void main(String[] args) { + Person p = new Student(); + p.run(); +} + +``` + +注:子类继承父类,调用a方法,如果a方法在子类中没有重写,那么就是调用的是子类继承父类的a方法, 如果重写了,那么调用的就是重写之后的方法。 + +子类中独有方法的调用 + +```java +public class Person{ + public void run(){} +} + +public class Student extends Person{ + public void test(){ + } +} + +main: +Person p = new Student(); +//调用到继承的run方法 +p.run(); + +//编译报错,因为编译器检查变量p的类型是Person,但是在Person类中并没有发现test方法,所以编译报错. +p.test(); +``` + +注:一个变量x,调用一个方法test,编译器是否能让其编译通过,主要是看声明变量x的类型中有没有定义 test方法,如果有则编译通过,如果没有则编译报错。而不是看x所指向的对象中有没有test方法。 + +原理:编译看左边,运行不一定看右边。 + +> 编译看左边的意思:java 编译器在编译的时候会检测引用类型中含有指定的成员,如果没有就会报错。 子类的成员是特有的,父类的没有的,所以他是找不到的。 +> +> 所以看左边,Person 中没有test()方法,于是编译报错 + +**子类引用和父类引用指向对象的区别** + +```java +Student s = new Student(); +Person p = new Student(); +``` + +变量s能调用的方法是Student中有的方法(包括继承过来的),变量p能调用的方法是Person中有的方法(包括继承过来的)。 + +但是变量p是父类型的,p不仅可以指向Student对象,还可以指向Teacher类型对象等,但是变量s只能指 Studnet类型对象,及Student子类型对象。变量p能指向对象的范围是比变量s大的。 + +Object类型的变量o,能指向所有对象,它的范围最大,但是使用变量o能调用到的方法也是最少的,只能调用到Object中的声明的方法,因为变量o声明的类型就是Object。 + +注:java中的方法调用,是运行时动态和对象绑定的,不到运行的时候,是不知道到底哪个方法被调用的。 + +**多态的好处与弊端** + +- 好处:提高了程序的拓展性 + + 具体表现:定义方法的时候,使用父类型作为参数,将来在使用的时候,使用具体的子类型参与操作 + +- 弊端:不能使用子类的特有功能 + +### 2、重写、重载和多态的关系 + +重载是编译时多态 + +> 调用重载的方法,在编译期间就要确定调用的方法是谁,如果不能确定则编译报错 + +重写是运行时多态 + +> 调用重写的方法,在运行期间才能确定这个方法到底是哪个对象中的。这个取决于调用方法的引用,在运行 期间所指向的对象是谁,这个引用指向哪个对象那么调用的就是哪个对象中的方法。(java中的方法调用,是运行时动态和对象绑定的) + +### 3、多态的注意事项 + +1. 多态是方法的多态,属性没有多态性。 +2. 编写程序时,如果想调用运行时类型的方法,只能进行类型转换。不然通不过编译器的检查。但是如果两个没有关联的类进行强制转换,会报:ClassCastException。 比如:本来是狗,我把它转成猫。就会报这个异常。 +3. 多态的存在要有3个必要条件:要有继承,要有方法重写,父类引用指向子类对象 + + + +### 4、多态存在的条件 + +- 有继承关系 +- 子类重写父类方法 +- 父类引用指向子类对象 + +补充一下第二点,既然多态存在必须要有“子类重写父类方法”这一条件,那么以下三种类型的方法是没有办法表现出多态特性的(因为不能被重写): + +1. static方法,因为被static修饰的方法是属于类的,而不是属于实例的 +2. final方法,因为被final修饰的方法无法被子类重写 +3. private方法和protected方法,前者是因为被private修饰的方法对子类不可见,后者是因为尽管被 protected修饰的方法可以被子类见到,也可以被子类重写,但是它是无法被外部所引用的,一个不能被外部引用的方法,怎么能谈多态呢 + +### 5、方法绑定(method binding) + +执行调用方法时,系统根据相关信息,能够执行内存地址中代表该方法的代码。分为静态绑定和动态绑定。 + +**静态绑定:** + +在编译期完成,可以提高代码执行速度。 + +**动态绑定:** + +通过对象调用的方法,采用动态绑定机制。这虽然让我们编程灵活,但是降低了代码的执行速度。这也是JAVA比C/C++速度慢的主要因素之一。JAVA中除了final类、final方、static方法,所有方法都是JVM在运行期才进行动态绑定的。 + +多态:如果编译时类型和运行时类型不一致,就会造成多态。 + +### 6、instanceof和类型转换 + +**1、instanceof** + +三个不同java文件 + +```java +public class Person{ + public void run(){} +} + +public class Student extends Person{ +} + +public class Teacher extends Person{ +} +``` + +main:方法下 + +```java +Object o = new Student(); +System.out.println(o instanceof Student);//true +System.out.println(o instanceof Person);//true +System.out.println(o instanceof Object);//true +System.out.println(o instanceof Teacher);//false +System.out.println(o instanceof String);//false +``` + +```java +Person o = new Student(); +System.out.println(o instanceof Student);//true +System.out.println(o instanceof Person);//true +System.out.println(o instanceof Object);//true +System.out.println(o instanceof Teacher);//false +//编译报错 +System.out.println(o instanceof String); +``` + +```java +Student o = new Student(); +System.out.println(o instanceof Student);//true +System.out.println(o instanceof Person);//true +System.out.println(o instanceof Object);//true +//编译报错 +System.out.println(o instanceof Teacher); +//编译报错 +System.out.println(o instanceof String); +``` + +> **System.out.println(x instanceof Y);** + +【分析1】 + +该代码能否编译通过,主要是看声明变量x的类型和Y是否存在子父类的关系。有"子父类关"系就编译通过,没有子父类关系就是编译报错。 + +之后学习到的接口类型和这个是有点区别的。 + +【分析2】 + +输出结果是true还是false,主要是看变量x所指向的对象实际类型是不是Y类型的"子类型". + +```java +Object o = new Person(); +System.out.println(o instanceof Student);//false +System.out.println(o instanceof Person);//true +System.out.println(o instanceof Object);//true +System.out.println(o instanceof Teacher);//false +System.out.println(o instanceof String);//false +``` + +**2、类型转换** + +```java +public class Person{ + public void run(){} +} + +public class Student extends Person{ + public void go(){} +} + +public class Teacher extends Person{ +} +``` + +**为什么要类型转换** + +```java +//编译报错,因为p声明的类型Person中没有go方法 +Person p = new Student(); +p.go(); + +//需要把变量p的类型进行转换 +Person p = new Student(); +Student s = (Student)p; +s.go(); + +//或者 +//注意这种形式前面必须要俩个小括号 +Person p = new Student(); +((Student)p).go(); +``` + +**类型转换中的问题** + +```java +//编译通过 运行没问题 +Object o = new Student(); +Person p = (Person)o; + +//编译通过 运行没问题 +Object o = new Student(); +Student s = (Student)o; + +//编译通过,运行报错 +Object o = new Teacher(); +Student s = (Student)o; + +``` + +即: X x = (X)o; + +运行是否报错,主要是变量o所指向的对象实现类型,是不是X类型的子类型,如果不是则运行就会报错。 + +**【总结】** + +1. 父类引用可以指向子类对象,子类引用不能指向父类对象。 + +2. 把子类对象直接赋给父类引用叫向上转型(upcasting),不用强制转型。 + + 如Father father = new Son(); + +3. 把指向子类对象的父类引用赋给子类引用叫向下转型(downcasting),要强制转型。 + + 如father就是一个指向子类对象的父类引用,把father赋给子类引用son + + 即Son son =(Son) father; + + 其中father前面的(Son)必须添加,进行强制转换。 + +4. upcasting 会丢失子类特有的方法,但是子类overriding 父类的方法,子类方法有效 + +5. 向上转型的作用,减少重复代码,父类为参数,调有时用子类作为参数,就是利用了向上转型。这样使代码变得简洁。体现了JAVA的抽象编程思想。 + +## 修饰符 + +### 1、static修饰符 + +**1、static变量** + +在类中,使用static修饰的成员变量,就是静态变量,反之为非静态变量。 + +**静态变量和非静态变量的区别** + +静态变量属于类的,"可以"使用类名来访问,非静态变量是属于对象的,"必须"使用对象来访问。 + +```java +public class Student{ + private static int age; + private double score; + public static void main(String[] args) { + Student s = new Student(); + //推荐使用类名访问静态成员 + System.out.println(Student.age); + System.out.println(s.age); + + System.out.println(s.score); + } +} +``` + +静态变量对于类而言在内存中只有一个,能被类的所有实例所共享。实例变量对于类的每个实例都有一份, 它们之间互不影响。(在**基础语法**中粗略解释过静态变量) + +``` +public class Student { + private static int count; + private int num; + + public Student() { + count++; + num++; + } + + public static void main(String[] args) { + Student s1 = new Student(); + Student s2 = new Student(); + Student s3 = new Student(); + Student s4 = new Student(); + //因为还是在类中,所以可以直接访问私有属性 + System.out.println(s1.num);//1 + System.out.println(s2.num); + System.out.println(s3.num); + System.out.println(s4.num); + System.out.println(Student.count); + + System.out.println(s1.count);//4 + System.out.println(s2.count); + System.out.println(s3.count); + System.out.println(s4.count); + } +} +``` + +在加载类的过程中为静态变量分配内存,实例变量在创建对象时分配内存,所以静态变量可以使用类名来直接访问,而不需要使用对象来访问。 + +**2、static方法** + +在类中,使用static修饰的成员方法,就是静态方法,反之为非静态方法。 + +**静态方法和非静态方法的区别** + +> 静态方法数属于类的,"可以"使用类名来调用,非静态方法是属于对象的,"必须"使用对象来调用。 + +静态方法"不可以"直接访问类中的非静态变量和非静态方法,但是"可以"直接访问类中的静态变量和静态方法 + +注意:this和super在类中属于非静态的变量.(静态方法中不能使用) + +``` +public class Student { + private static int count; + private int num; + public void run(){} + public static void go(){} + public static void test(){ + //编译通过 + System.out.println(count); + go(); + + //编译报错 + System.out.println(num); + run(); + } +} +``` + +非静态方法"可以"直接访问类中的非静态变量和非静态方法,也"可以"直接访问类中的静态变量和静态方法 + +``` +public class Student { + private static int count; + private int num; + + public void run() { + } + + public static void go() { + } + public void test() { + //编译通过 + System.out.println(count); + go(); + //编译通过 + System.out.println(num); + run(); + } +} +``` + +思考:为什么静态方法和非静态方法不能直接相互访问? 加载顺序的问题! + +父类的静态方法可以被子类继承,但是不能被子类重写 + +```java +public class Person { + public static void method() {} +} + +//编译报错 +public class Student extends Person { + public void method(){} +} + +``` + +```java +public class Person { + public static void test() { + System.out.println("Person"); + } +} +//编译通过,但不是重写 +public class Student extends Person { + public static void test(){ + System.out.println("Student"); + } +} +``` + +```java +//main: +Perosn p = new Student(); +p.test();//输出Person +p = new Person(); +p.test();//输出Perosn +``` + +父类的非静态方法不能被子类重写为静态方法 ; + +```java +public class Person { + public void test() { + System.out.println("Person"); + } +} +//编译报错 +public class Student extends Person { + public static void test(){ + System.out.println("Student"); + } +} +``` + +**3、代码块和静态代码块** + +【类中可以编写代码块和静态代码块】 + +```java +public class Person { + { + //代码块(匿名代码块) + } + static{ + //静态代码块 + } +} +``` + +【匿名代码块和静态代码块的执行】 + +因为没有名字,在程序并不能主动调用这些代码块。 + +匿名代码块是在创建对象的时候自动执行的,并且在构造器执行之前,在静态代码块之后。同时匿名代码块在每次创建对象的时候都会自动执行。 + +静态代码块是在类加载完成之后就自动执行,并且只执行一次。 + +注:每个类在第一次被使用的时候就会被加载,并且一般只会加载一次。 + +```java +public class Student { + { + System.out.println("匿名代码块"); + } + + static{ + System.out.println("静态代码块"); + } + + public Student(){ + System.out.println("构造器"); + } +} +``` + +```java +//main: +Student s1 = new Student(); +Student s2 = new Student(); +Student s3 = new Student(); +``` + +输出: + +> 静态代码块 +> 匿名代码块 +> 构造器 +> +> 匿名代码块 +> 构造器 +> +> 匿名代码块 +> 构造器 + +【匿名代码块和静态代码块的作用】 + +匿名代码块的作用是给对象的成员变量初始化赋值,但是因为构造器也能完成这项工作,所以匿名代码块使用的并不多。 + +静态代码块的作用是给类中的静态成员变量初始化赋值。 + +```java +public class Person { + public static String name; + static{ + name = "tom"; + } + public Person(){ + name = "zs"; + } +} +``` + +> main: +> System.out.println( Person.name ); //tom + +**注**:在构造器中给静态变量赋值,并不能保证能赋值成功,因为构造器是在创建对象的时候才指向,但是静态变量可以不创建对象而直接使用类名来访问。 + +**4、创建和初始化对象的过程** + +```java +Student s = new Student(); +``` + +【Student类之前没有进行类加载的过程】 + +1. 类加载,同时初始化类中静态的属性 +2. 执行静态代码块 +3. 分配内存空间,同时初始化非静态的属性(赋默认值,0/false/null) +4. 调用Student的父类构造器 +5. 对Student中的属性进行显示赋值(如果有的话) +6. 执行匿名代码块 +7. 执行构造器 +8. 返回内存地址 + +注:子类中非静态属性的显示赋值是在父类构造器执行完之后和子类中的匿名代码块执行之前的时候 + +```java +public class Person{ + private String name = "zs"; + public Person() { + System.out.println("Person构造器"); + print(); + } + public void print(){ + System.out.println("Person print方法: name = "+name); + } +} +``` + +```java +public class Student extends Person{ + private String name = "tom"; + { + System.out.println("Student匿名代码块"); + } + static{ + System.out.println("Student静态代码块"); + } + public Student(){ + System.out.println("Student构造器"); + } + public void print(){ + System.out.println("student print方法: name = "+name); + } + public static void main(String[] args) { + new Student(); + } +} +``` + +输出: + +>Student静态代码块 +>Person构造器 +>student print方法: name = null +>Student匿名代码块 +>Student构造器 + +```java +Student s = new Student(); +//Student类之前已经进行了类加载 +//1.分配内存空间,同时初始化非静态的属性(赋默认值,0/false/null) +//2.调用Student的父类构造器 +//3.对Student中的属性进行显示赋值(如果有的话) +//4.执行匿名代码块 +//5.执行构造器 +//6.返回内存地址 +``` + +**5、静态导入** + +静态导包就是java包的静态导入,用import static代替import静态导入包是JDK1.5中的新特性。 + +意思是导入这个类里的静态方法。 + +好处:这种方法的好处就是可以简化一些操作,例如打印操作System.out.println(…);就可以将其写入一 个静态方法print(…),在使用时直接print(…)就可以了。但是这种方法建议在有很多重复调用的时候使用,如果仅有一到两次调用,不如直接写来的方便。 + +```java +import static java.lang.Math.random; +import static java.lang.Math.PI; + +public class Test { + public static void main(String[] args) { + //之前是需要Math.random()调用的 + System.out.println(random()); + System.out.println(PI); + } +} +``` + +### 2、final修饰符 + +**1、修饰类** + +用final修饰的**类**不能被继承,没有子类 + +例如:我们是无法写一个类去继承String类,然后对String类型扩展的。因为API中已经被String类定义为final + +我们也可以定义final修饰的类: + +```java +public final class Action{ + +} + +//编译报错 +public class Go extends Action{ + +} +``` + +**2、修饰方法** + +用final修饰的**方法**可以被继承,但是不能被子类的重写。 + +例如:每个类都是Object类的子类,继承了Object中的众多方法,在子类中可以重写toString方法、equals方法等,但是不能重写getClass方法、wait方法等,因为这些方法都是使用fianl修饰的。 + +我们也可以定义final修饰的方法: + +```java +public class Person{ + public final void print(){} +} + +//编译报错 +public class Student extends Person{ + public void print(){ + } +} +``` + +**3、修饰变量** + +用final修饰的**变量**表示**常量**,只能被赋一次值。使用final修饰的变量也就成了常量了,因为值不会再变了。 + +【修饰局部变量】 + +```java +public class Person{ + public void print(final int a){ + //编译报错,不能再次赋值,传参的时候已经赋过了 + a = 1; + } +} + +public class Person{ + public void print(){ + final int a; + a = 1; + //编译报错,不能再次赋值 + a = 2; + } +} +``` + +【修饰成员变量-非静态成员变量】 + +```java +public class Person{ + private final int a; +} +/* +只有一次机会,可以给此变量a赋值的位置: +声明的同时赋值 +匿名代码块中赋值 +构造器中赋值(类中出现的所有构造器都要写) +*/ +``` + +【修饰成员变量-静态成员变量】 + +```java +public class Person{ + private static final int a; +} +/* +只有一次机会,可以给此变量a赋值的位置: +声明的同时赋值 +静态代码块中赋值 +*/ +``` + +【修饰引用对象】 + +```java +final Student s = new Student(); +//编译通过 +s.setName("tom"); +s.setName("zs"); + +//编译报错,不能修改引用s指向的内存地址 +s = new Student(); +``` + +### 3、abstract修饰符 + + abstract修饰符可以用来修饰方法也可以修饰类,如果修饰方法,那么该方法就是抽象方法。如果修饰类,那么该类就是抽象类。 + +**1、抽象类和抽象方法的关系** + +抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类。 + +**2、语法** + +```java +public abstract class Action{ + public abstract void doSomething(); +} + +public void doSomething(){...} +``` + +对于这个普通方法来讲: + +"public void doSomething()"这部分是方法的声明。 +"{...}"这部分是方法的实现,如果大括号中什么都没写,就叫方法的空实现 + +声明类的同时,加上abstract修饰符就是抽象类 +声明方法的时候,加上abstract修饰符,并且去掉方法的大口号,同时结尾加上分号,该方法就是抽象方法。 + +### 3、特点及作用 + +抽象类,不能使用new关键字来创建对象,它是用来让子类继承的。 +抽象方法,只有方法的声明,没有方法的实现,它是用来让子类实现的。 + +注:子类继承抽象类后,需要实现抽象类中没有实现的抽象方法,否则这个子类也要声明为抽象类。 + +```java +public abstract class Action{ + public abstract void doSomething(); +} + +main: +//编译报错,抽象类不能new对象 +Action a = new Action(); + +//子类继承抽象类 +public class Eat extends Action{ + //实现父类中没有实现的抽象方法 + public void doSomething(){ + //code + } +} + +main: +Action a = new Eat(); +a.doSomething(); +``` + +注:子类继承抽象类,那么就必须要实现抽象类没有实现的抽象方法,否则该子类也要声明为抽象类。 + +**4、思考** + +思考1 : 抽象类不能new对象,那么抽象类中有没有构造器? + +> 抽象类是不能被实例化,抽象类的目的就是为实现多态中的共同点,抽象类的构造器会在子类实例化时调用,因此它也是用来实现多态中的共同点构造,不建议这样使用! + +思考2 : 抽象类和抽象方法意义(为什么要编写抽象类、抽象方法) + +> 打个比方,要做一个游戏。如果要创建一个角色,如果反复创建类和方法会很繁琐和麻烦。建一个抽象类 +> 后。若要创建角色可直接继承抽象类中的字段和方法,而抽象类中又有抽象方法。如果一个角色有很多种 +> 职业,每个职业又有很多技能,要是依次实例这些技能方法会显得想当笨拙。定义抽象方法,在需要时继 +> 承后重写调用,可以省去很多代码。 +> +> 总之抽象类和抽象方法起到一个框架作用。很方便后期的调用和重写 +> 抽象方法是为了程序的可扩展性。重写抽象方法时即可实现同名方法但又非同目的的要求。 + +## 接口 + +### 1、接口的本质 + +普通类:只有具体实现 +抽象类:具体实现和规范(抽象方法) 都有! +接口:只有规范! + +**为什么需要接口?接口和抽象类的区别?** + +- 接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。 +- 抽象类还提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。 +- 从接口的实现者角度看,接口定义了可以向外部提供的服务。 +- 从接口的调用者角度看,接口定义了实现者能提供那些服务。 +- 接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的系统之间模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。 + +**接口的本质探讨** + +- 接口就是规范,定义的是一组规则,体现了现实世界中”如果你是…则必须能…“的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。 +- 接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守。 +- OO的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如c++、java、c#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。 + +### 2、接口与抽象类的区别 + +抽象类也是类,除了可以写抽象方法以及不能直接new对象之外,其他的和普通类没有什么不一样的。接口已经另一种类型了,和类是有本质的区别的,所以不能用类的标准去衡量接口。 + +**声明类的关键字是class,声明接口的关键字是interface。** + +抽象类是用来被继承的,java中的类是单继承。 + +类A继承了抽象类B,那么类A的对象就属于B类型了,可以使用多态 +一个父类的引用,可以指向这个父类的任意子类对象 +注:继承的关键字是extends + +接口是用来被类实现的,java中的接口可以被多实现。 +类A实现接口B、C、D、E..,那么类A的对象就属于B、C、D、E等类型了,可以使用多态 +一个接口的引用,可以指向这个接口的任意实现类对象 +注:实现的关键字是implements + +### 3、接口中的方法都是抽象方法 + +接口中可以不写任何方法,但如果写方法了,该方法必须是抽象方法 + +```java +public interface Action{ + public abstract void run(); + + //默认就是public abstract修饰的 + void test(); + public void go(); +} +``` + +### 4、接口中的变量都是静态常量 + +public static final修饰 + +接口中可以不写任何属性,但如果写属性了,该属性必须是public static final修饰的静态常量。 +注:可以直接使用接口名访问其属性。因为是public static修饰的 + +注:声明的同时就必须赋值(因为接口中不能编写静态代码块) + +```java +public interface Action{ + public static final String NAME = "tom"; + //默认就是public static final修饰的 + int AGE = 20; +} + +main: +System.out.println(Action.NAME); +System.out.println(Action.AGE); +``` + +### 5、一个类可以实现多个接口 + +```java +public class Student implements A,B,C,D{ + //Student需要实现接口A B C D中所有的抽象方法 + //否则Student类就要声明为抽象类,因为有抽象方法没实现 +} + +main: +A s1 = new Student(); +B s2 = new Student(); +C s3 = new Student(); +D s4 = new Student(); +``` + +注: +s1只能调用接口A中声明的方法以及Object中的方法 +s2只能调用接口B中声明的方法以及Object中的方法 +s3只能调用接口C中声明的方法以及Object中的方法 +s4只能调用接口D中声明的方法以及Object中的方法 + +注:必要时可以类型强制转换 + +例如 : 接口A 中有test() , 接口B 中有run() + +### 6、一个接口可以继承多个父接口 + +```java +public interface A{ + public void testA(); +} + +public interface B{ + public void testB(); +} + +//接口C把接口A B中的方法都继承过来了 +public interface C extends A,B{ + public void testC(); +} + +//Student相当于实现了A B C三个接口,需要实现所有的抽象方法 +//Student的对象也就同时属于A类型 B类型 C类型 +public class Student implements C{ + public viod testA(){} + public viod testB(){} + public viod testC(){} +} + +main: + +C o = new Student(); +System.out.println(o instanceof A);//true +System.out.println(o instanceof B);//true +System.out.println(o instanceof C);//true +System.out.println(o instanceof Student);//true +System.out.println(o instanceof Object);//true +System.out.println(o instanceof Teacher);//false + +//编译报错 +System.out.println(o instanceof String); +``` + +注:System.out.println(o instanceof X); + +> 如果o是一个接口类型声明的变量,那么只要X不是一个final修饰的类,该代码就能通过编译,至于其结果是不是true,就要看变量o指向的对象的实际类型,是不是X的子类或者实现类了。 + +注:一个引用所指向的对象,是有可能实现任何一个接口的。(java中的多实现) + +### 7、接口的作用 + +接口的最主要的作用是达到统一访问,就是在创建对象的时候用接口创建 + +【接口名】 【对象名】= new 【实现接口的类】 + +这样你像用哪个类的对象就可以new哪个对象了,不需要改原来的代码。 + +假如我们两个类中都有个function()的方法,如果我用接口,那样我new a();就是用a的方法,new b() 就是用b的方法。 + +这个就叫统一访问,因为你实现这个接口的类的方法名相同,但是实现内容不同 + +总结: + +1. Java接口中的成员变量默认都是public,static,final类型的(都可省略),必须被显示初始化,即接口中的成员变量为常量(大写,单词之间用"_"分隔) +2. Java接口中的方法默认都是public,abstract类型的(都可省略),没有方法体,不能被实例化 +3. Java接口中只能包含public,static,final类型的成员变量和public,abstract类型的成员方法 +4. 接口中没有构造方法,不能被实例化 +5. 一个接口不能实现(implements)另一个接口,但它可以继承多个其它的接口 +6. Java接口必须通过类来实现它的抽象方法 +7. 当类实现了某个Java接口时,它必须实现接口中的所有抽象方法,否则这个类必须声明为抽象类 +8. 不允许创建接口的实例(实例化),但允许定义接口类型的引用变量,该引用变量引用实现了这个接口的类的实例 +9. 一个类只能继承一个直接的父类,但可以实现多个接口,间接的实现了多继承。 + +```java +interface SwimInterface{ + void swim(); +} + +class Fish{ + int fins=4; +} + +class Duck { + int leg=2; + void egg(){}; +} + +class Goldfish extends Fish implements SwimInterface { + @Override + public void swim() { + System.out.println("Goldfish can swim "); + } +} + +class SmallDuck extends Duck implements SwimInterface { + public void egg(){ + System.out.println("SmallDuck can lay eggs "); + } + @Override + public void swim() { + System.out.println("SmallDuck can swim "); + } +} + +public class InterfaceDemo { + public static void main(String[] args) { + Goldfish goldfish=new Goldfish(); + goldfish.swim(); + + SmallDuck smallDuck= new SmallDuck(); + smallDuck.swim(); + smallDuck.egg(); + } +} +``` + +## 内部类 + +上一小节,我们学习了接口,在以后的工作中接口是我们经常要碰到的,所以一定要多去回顾。接下来介绍一下内部类。很多时候我们创建类的对象的时候并不需要使用很多次,每次只使用一次,这个时候我们就可以使用内部类了。 + +### 1、内部类概述 + +内部类就是在一个类的内部在定义一个类,比如,A类中定义一个B类,那么B类相对A类来说就称为内部类,而A类相对B类来说就是外部类了。 + +内部类不是在一个java源文件中编写俩个平行的俩个类,而是在一个类的内部再定义另外一个类。 我们可以把外边的类称为外部类,在其内部编写的类称为内部类。 + +内部类分为四种: + +1. 成员内部类 +2. 静态内部类 +3. 局部内部类 +4. 匿名内部类 + +### 2、成员内部类 + +**成员内部类(实例内部类、非静态内部类)** + +注:成员内部类中不能写静态属性和方法 + +【定义一个内部类】 + +```java +//在A类中申明了一个B类,此B类就在A的内部,并且在成员变量的位置上,所以就称为成员内部类 +public class Outer { + private int id; + public void out(){ + System.out.println("这是外部类方法"); + } + + class Inner{ + public void in(){ + System.out.println("这是内部类方法"); + } + } +} +``` + +【实例化内部类】 + +实例化内部类,首先需要实例化外部类,通过外部类去调用内部类 + +```java +public class Outer { + private int id; + public void out(){ + System.out.println("这是外部类方法"); + } + + class Inner{ + public void in(){ + System.out.println("这是内部类方法"); + } + } +} + +public class Test{ + public static void main(String[] args) { + //实例化成员内部类分两步 + //1、实例化外部类 + Outer outObject = new Outer(); + //2、通过外部类调用内部类 + Outer.Inner inObject = outObject.new Inner(); + //测试,调用内部类中的方法 + inObject.in();//打印:这是内部类方法 + } +} +``` + +分析:想想如果你要使用一个类中方法或者属性,你就必须要先有该类的一个对象,同理,一个类在另 一个类的内部,那么想要使用这个内部类,就必须先要有外部类的一个实例对象,然后在通过该对象去使用内部类。 + +【成员内部类能干什么?】 + +- 访问外部类的所有属性(这里的属性包括私有的成员变量,方法) + +```java +public class Outer { + private int id; + public void out(){ + System.out.println("这是外部类方法"); + } + + class Inner{ + public void in(){ + System.out.println("这是内部类方法"); + } + //内部类访问外部类私有的成员变量 + public void useId(){ + System.out.println(id+3);。 + } + //内部类访问外部类的方法 + public void useOut(){ + out(); + } + } +} + +public class Test{ + public static void main(String[] args) { + //实例化成员内部类分两步 + //1、实例化外部类 + Outer outObject = new Outer(); + //2、通过外部类调用内部类 + Outer.Inner inObject = outObject.new Inner(); + //测试 + inObject.useId();//打印3,因为id初始化值为0,0+3就为3,其中在内部类就使用了外部类的私有成员变量id。 + inObject.useOut();//打印:这是外部类方法 + } +} +``` + +- 如果内部类中的变量名和外部类的成员变量名一样,要通过创建外部类对象"."属性来访问外部类属性,通过this.属性访问内部类成员属性 + +```java +public class Outer { + private int id;//默认初始化0 + + public void out() { + System.out.println("这是外部类方法"); + } + + class Inner { + private int id = 8; //这个id跟外部类的属性id名称一样。 + + public void in() { + System.out.println("这是内部类方法"); + } + + public void test() { + System.out.println(id);//输出8,内部类中的变量会暂时将外部类的成员变量给隐藏 + // 如何调用外部类的成员变量呢?通过Outer.this + // 想要知道为什么能通过这个来调用,就得明白下面这个原理 + // 想实例化内部类对象,就必须通过外部类对象,当外部类对象来new出内部类对象时,会把自己(外部类对象)的引用传到了内部类中, + // 所以内部类就可以通过Outer.this来访问外部类的属性和方法 + // 到这里,你也就可以知道为什么内部类可以访问外部类的属性和方法,这里由于有两个相同的属性名称, + + // 所以需要显示的用Outer.this来调用外部类的属性,平常如果属性名不重复 + // 那么我们在内部类中调用外部类的属性和方法时,前面就隐式的调用了Outer.this。 + System.out.println(Outer.this.id);//输出外部类的属性id。也就是输出0 + } + } +} +``` + +借助成员内部类,来总结内部类(包括4种内部类)的通用用法: + +1. 要想访问内部类中的内容,必须通过外部类对象来实例化内部类。 + +2. 能够访问外部类所有的属性和方法,原理就是在通过外部类对象实例化内部类对象时,外部类对象把自己的引用传进了内部类,使内部类可以用通过Outer.this去调用外部类的属性和方法。 + + 一般都是隐式调用了,但是当内部类中有属性或者方法名和外部类中的属性或方法名相同的时候,就需要通过显式调用Outer.this了。 + +【写的一个小例子】 + +```java +public class MemberInnerClassTest { + private String name; + private static int age; + + public void run() { + } + + public static void go() { + } + + public class MemberInnerClass { + private String name; + + //内部类访问外部类 + public void test(String name) { + System.out.println(name); + System.out.println(this.name); + System.out.println(MemberInnerClassTest.this.name); + System.out.println(MemberInnerClassTest.age); + MemberInnerClassTest.this.run(); + MemberInnerClassTest.go(); + } + } + + //外部类访问成员内部类 + //成员内部类的对象要 依赖于外部类的对象的存在 + public void test() { + //MemberInnerClass mic = MemberInnerClassTest.this.new MemberInnerClass(); + //MemberInnerClass mic = this.new MemberInnerClass(); + MemberInnerClass mic = new MemberInnerClass(); + mic.name = "tom"; + mic.test("hua"); + } + + public static void main(String[] args) { + //MemberInnerClass mic = new MemberInnerClass();这个是不行的,this是动态的。 + //所以要使用要先创建外部类对象,才能使用 + MemberInnerClassTest out = new MemberInnerClassTest(); + MemberInnerClass mic = out.new MemberInnerClass(); + //如果内部类是private,则不能访问,只能铜鼓内部方法来调用内部类 + mic.name = "jik"; + mic.test("kkk"); + } +} +``` + +### 3、静态内部类 + +看到名字就知道,使用static修饰的内部类就叫静态内部类。 + +既然提到了static,那我们就来复习一下它的用法:一般只修饰变量和方法,平常不可以修饰类,但是内部类却可以被static修饰。 + +1. static修饰成员变量:整个类的实例共享静态变量 +2. static修饰方法:静态方法,只能够访问用static修饰的属性或方法,而非静态方法可以访问static修饰的方法或属性 +3. 被static修饰了的成员变量和方法能直接被类名调用。 +4. static不能修饰局部变量,切记,不要搞混淆了,static平常就用来修饰成员变量和方法。 + +例子: + +```java +public class StaticInnerClassTest { + + private String name; + private static int age; + + public void run() { + } + + public static void go() { + } + + //外部类访问静态内部类 + public void test() { + StaticInnerClass sic = new StaticInnerClass(); //静态的内部类不需要依赖外部类,所以不用this + sic.name = "tom"; + + sic.test1("jack"); + StaticInnerClass.age = 10; + StaticInnerClass.test2("xixi"); + } + + private static class StaticInnerClass { + private String name; + private static int age; + + public void test1(String name) { + System.out.println(name); + System.out.println(this.name); + System.out.println(StaticInnerClass.age); + System.out.println(StaticInnerClassTest.age); + //System.out.println(StaticInnerClassTest.this.name);静态类不能访问非静态属性 + StaticInnerClassTest.go(); + //StaticInnerClassTest.this.run();静态类不能访问非静态方法 + } + + public static void test2(String name) { + //只能访问自己和外部类的静态属性和方法 + System.out.println(name); + //System.out.println(this.name);静态方法里面连自己类的非静态属性都不能访问 + System.out.println(StaticInnerClass.age); + System.out.println(StaticInnerClassTest.age); + //System.out.println(StaticInnerClassTest.this.name);静态方法不能访问非静态属性 + StaticInnerClassTest.go(); + //StaticInnerClassTest.this.run();静态方法不能访问非静态方法 + } + } +} +``` + +注意: + +1. 我们上面说的内部类能够调用外部类的方法和属性,在静态内部类中就行了,因为静态内部类没有 了指向外部类对象的引用。除非外部类中的方法或者属性也是静态的。这就回归到了static关键字的用法。 + +2. 静态内部类能够直接被外部类给实例化,不需要使用外部类对象 + + ```jav + Outer.Inner inner = new Outer.Inner(); + ``` + +3. 静态内部类中可以声明静态方法和静态变量,但是非静态内部类中就不可以声明静态方法和静态变量 + +### 4、局部内部类 + +局部内部类是在一个方法内部声明的一个类 +局部内部类中可以访问外部类的成员变量及方法 +局部内部类中如果要访问该内部类所在方法中的局部变量,那么这个局部变量就必须是final修饰的 + +```java +public class Outer { + private int id; + + //在method01方法中有一个Inner内部类,这个内部类就称为局部内部类 + public void method01() { + class Inner { + public void in() { + System.out.println("这是局部内部类"); + } + } + } +} +``` + +局部内部类一般的作用跟在成员内部类中总结的差不多,但是有两个要注意的地方: + +**1、在局部内部类中,如果要访问局部变量,那么该局部变量要用final修饰** + +为什么需要使用final? + +final修饰变量:变为常量,会在常量池中放着,逆向思维想这个问题,如果不实用final修饰,当局部内部类被实例化后,方法弹栈,局部变量随着跟着消失,这个时候局部内部类对象在想去调用该局部变量,就会报错,因为该局部变量已经没了,当局部变量用fanal修饰后,就会将其加入常量池中,即使方法弹栈了,该局部变量还在常量池中呆着,局部内部类也就是够调用。所以局部内部类想要调用局部变 量时,需要使用final修饰,不使用,编译度通不过。 + +```java +public class Outer { + private int id; + + public void method01() { + //这个就是局部变量cid。要让局部内部类使用,就得变为final并且赋值,如果不使用final修饰,就会报错 + final int cid = 3; + class Inner { + //内部类的第一个方法 + public void in() { + System.out.println("这是局部内部类"); + } + + //内部类中的使用局部变量cid的方法 + public void useCid() { + System.out.println(cid); + } + } + } +} +``` + +**2、局部内部类不能通过外部类对象直接实例化,而是在方法中实例化出自己来,然后通过内部类对象 调用自己类中的方法。** + +看下面例子就知道如何用了。 + +```java +public class Outer { + private int id; + + public void out() { + System.out.println("外部类方法"); + } + + public void method01() { + class Inner { + public void in() { + System.out.println("这是局部内部类"); + } + } + //关键在这里,如需要在method01方法中自己创建内部类实例, + // 然后调用内部类中的方法,等待外部类调用method01方法, + // 就可以执行到内部类中的方法了。 + Inner In = new Inner(); + In.in(); + } +} +``` + +使用局部内部类需要注意的地方就刚才上面说的: + +1. 在局部内部类中,如果要访问局部变量,那么该局部变量要用final修饰 + +2. 如何调用局部内部类方法。 + + ```java + public class LocalInnerClassTest { + private String name; + private static int age; + + public void run() { + } + + public static void go() { + } + + //局部内部类要定义在方法中 + public void test() { + final String myname = ""; + class LocalInnerClass { + private String name; + + // private static int age;不能定义静态属性 + public void test(String name) { + System.out.println(name); + System.out.println(this.name); + System.out.println(myname); + System.out.println(LocalInnerClassTest.this.name); + LocalInnerClassTest.this.run(); + LocalInnerClassTest.go(); + } + } + // 局部内部类只能在自己的方法中用 + // 因为局部内部类相当于一个局部变量,出了方法就找不到了。 + LocalInnerClass lic = new LocalInnerClass(); + lic.name = "tom"; + lic.test("test"); + + } + + } + ``` + + + +### 5、匿名内部类 + +在这四种内部类中,以后的工作可能遇到最多的是匿名内部类,所以说匿名内部类是最常用的一种 内部类。 + +什么是匿名对象?如果一个对象只要使用一次,那么我们就是需要new Object().method()。 就可以了,而不需要给这个实例保存到该类型变量中去。这就是匿名对象。 + +```java +public class Test { + public static void main(String[] args) { + //讲new出来的Apple实例赋给apple变量保存起来,但是我们只需要用一次,就可以这样写 + Apple apple = new Apple(); + apple.eat(); + //这种就叫做匿名对象的使用,不把实例保存到变量中。 + new Apple().eat(); + } +} + +class Apple{ + public void eat(){ + System.out.println("我要被吃了"); + } +} +``` + +匿名内部类跟匿名对象是一个道理: + +匿名对象:我只需要用一次,那么我就不用声明一个该类型变量来保存对象了, + +匿名内部类:我也只需要用一次,那我就不需要在类中先定义一个内部类,而是等待需要用的时候,我就在临时实现这个内部类,因为用次数少,可能就这一次,那么这样写内部类,更方便。不然先写出一 个内部类的全部实现来,然后就调用它一次,岂不是用完之后就一直将其放在那,那就没必要那样。 + +1. 匿名内部类需要依托于其他类或者接口来创建 + - 如果依托的是类,那么创建出来的匿名内部类就默认是这个类的子类 + - 如果依托的是接口,那么创建出来的匿名内部类就默认是这个接口的实现类。 +2. 匿名内部类的声明必须是在使用new关键字的时候 + - 匿名内部类的声明及创建对象必须一气呵成,并且之后能反复使用,因为没有名字 + +【示例】 + +A是一个类(普通类、抽象类都可以),依托于A类创建一个匿名内部类对象 + +```java +main: + +A a = new A(){ + //实现A中的抽象方法 + //或者重写A中的普通方法 +}; + +注:这个大括号里面其实就是这个内部类的代码,只不过是声明该内部类的同时就是要new创建了其对象, +并且不能反复使用,因为没有名字。 + +例如: +B是一个接口,依托于B接口创建一个匿名内部类对象 + +B b = new B(){ + //实现B中的抽象方法 +}; +``` + +1. 匿名内部类除了依托的类或接口之外,不能指定继承或者实现其他类或接口,同时也不能被其他类所继承,因为没有名字。 +2. 匿名内部中,我们不能写出其构造器,因为没有名字。 +3. 匿名内部中,除了重写上面的方法外,一般不会再写其他独有的方法,因为从外部不能直接调用到。(间接是调用到的) + +```java +public interface Work{ + void doWork(); +} + +public class AnonymousOutterClass{ + private String name; + private static int age; + public void say(){} + public static void go(){} + + public void test(){ + final int i = 90; + + Work w = new Work(){ + public void doWork(){ + System.out.println(AnonymousOutterClass.this.name); + System.out.println(AnonymousOutterClass.age); + AnonymousOutterClass.this.say(); + AnonymousOutterClass.go(); + + System.out.println(i); + } + }; + w.doWork(); + } +} +``` + +我们可以试一下不 用匿名内部类 和 用匿名内部类 实现一个接口中的方法的区别 + +【不用匿名内部类】 + +```java +public class Test { + public static void main(String[] args) { + // 如果我们需要使用接口中的方法,我们就需要走3步, + // 1、实现接口 2、创建实现接口类的实例对象 3、通过对象调用方法 + //第二步 + Test02 test = new Test02(); + //第三步 + test.method(); + } +} + +//接口Test1 +interface Test01{ + public void method(); +} + +//第一步、实现Test01接口 +class Test02 implements Test01{ + @Override + public void method() { + System.out.println("实现了Test接口的方法"); + } +} +``` + +【使用匿名内部类】 + +```java +public class Test { + public static void main(String[] args) { + //如果我们需要使用接口中的方法,我们只需要走一步,就是使用匿名内部类,直接将其类的对象创建出来。 + new Test1(){ + public void method(){ + System.out.println("实现了Test接口的方法"); + } + }.method(); + } +} + +interface Test1{ + public void method(); +} +``` + +解析: + +其实只要明白一点,new Test1( ){ 实现接口中方法的代码 }; + +Test1(){...} 这个的作用就是将接口给实现了,只不过这里实现该接口的是一个匿名类,也就是说这个类没名字,只能使用这一次,我们知道了这是一个类, 将其new出来,就能获得一个实现了Test1接口的类的实例对象,通过该实例对象,就能调用该类中的方法了,因为其匿名类是在一个类中实现的,所以叫其匿名内部类。 + +不要纠结为什么 Test1( ){...} 就相当于实现了Test1接口,这其中的原理等足够强大了,在去学习,不要钻牛角尖,这里就仅仅是需要知道他的作用是什么,做了些什么东西就行。 + diff --git "a/docs/01.Java/04.Java-\345\270\270\347\224\250\347\261\273/08.\345\270\270\347\224\250\347\261\273.md" "b/docs/01.Java/04.Java-\345\270\270\347\224\250\347\261\273/08.\345\270\270\347\224\250\347\261\273.md" new file mode 100644 index 00000000..1936e6d1 --- /dev/null +++ "b/docs/01.Java/04.Java-\345\270\270\347\224\250\347\261\273/08.\345\270\270\347\224\250\347\261\273.md" @@ -0,0 +1,2009 @@ +--- +title: 常用类 +date: 2021-04-15 22:47:14 +permalink: /java/se/commonly-used-class/ +categories: + - java + - java-se +--- + + + +# JavaSE-常用类 + +[[toc]] + + + +## Object类 + + +理论上Object类是所有类的父类,即直接或间接的继承java.lang.Object类。 + +由于所有的类都继承在Object类,因此省略了extends Object关键字。 + +该类中主要有以下方法: + +- toString( ) +- getClass( ) +- equals( ) +- clone( ) +- finalize( ) + +其中toString(),getClass(),equals是其中最重要的方法。 + +查看Object类源码 + +![image-20210329172035497](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210329172035497.png) + +看图可知,在jdk15中最后一个方法过时了,但我们运营一般都是jdk1.8 + +**注意: Object类中的getClass(),notify(),notifyAll(),wait()等方法被定义为final类型,因此不能重写。** + +### 1 clone() 方法 + +详解文章:https://blog.csdn.net/zhangjg_blog/article/details/18369201#0-qzone-1-28144-d020d2d2a4e8d1a374a433f596ad1440 + +```java + protected native Object clone() throws CloneNotSupportedException; +``` + +clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象。所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象。那么在java语言中,有几种方式可以创建对象呢? + +- 使用new操作符创建一个对象 +- 使用clone方法复制一个对象 + +那么这两种方式有什么相同和不同呢? new操作符的本意是分配内存。程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后, 再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。而clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。 + +**clone与copy的区别** + +假设现在有一个Employee对象,Employee tobby = new Employee(“CMTobby”,5000) + +通常我们会有这样的赋值Employee cindyelf = tobby,这个时候只是简单了copy了一下reference。cindyelf和tobby都指向内存中同一个object,这样cindyelf或者tobby的一个操作都可能影响到对方。打个比方,如果我们通过cindyelf.raiseSalary()方法改变了salary域的值,那么tobby通过getSalary()方法,得到的就是修改之后的salary域的值,显然这不是我们愿意看到的。我们希望得到tobby的一个精确拷贝,同时两者互不影响,这时候, 我们就可以使用Clone来满足我们的需求。Employee cindy=tobby.clone(),这时会生成一个新的Employee对象,并且和tobby具有相同的属性值和方法。 + +**Shallow Clone与Deep Clone** + + 浅克隆和深克隆 + +- **浅克隆:** + + 是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。 + +- **深克隆:** + + 不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。 + +举例来说更加清楚。 + +![image-20210329221102470](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210329221102470.png) + +主要是JAVA里除了8种基本类型传参数是值传递,其他的类对象传参数都是引用,我们有时候不希望在方法里将参数改变,这是就需要在类中复写clone方法(实现深复制)。 + +Clone是如何完成的呢?Object在对某个对象实施Clone时对其是一无所知的,它仅仅是简单地执行域对域的copy,这就是Shallow Clone。这样,问题就来了咯。 + +以Employee为例,它里面有一个域hireDay不是基本数据类型的变量,而是一个reference变量,经过Clone之后就会产生一个新的Date型的reference, + +它和原始对象中对应的域指向同一个Date对象,这样克隆类就和原始类共享了一部分信息,而这样显然是不利的,过程下图所示: + +![image-20210329220750868](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210329220750868-1617026885587.png) + +这个时候我们就需要进行deep Clone了,对那些非基本类型的域进行特殊的处理,例如本例中的 hireDay。我们可以重新定义Clone方法,对hireDay做特殊处理,如下代码所示: + +```java +class Employee implements Cloneable { + public Object clone() throws CloneNotSupportedException { + Employee cloned = (Employee) super.clone(); + cloned.hireDay = (Date) hireDay.clone() + return cloned; + } +} +``` + +**clone方法的保护机制** + +在Object中Clone()是被声明为protected的,这样做是有一定的道理的,以Employee类为例,通过声明为protected,就可以保证只有Employee类里面才能“克隆”Employee对象。 + +**clone方法的使用** + +什么时候使用shallow Clone,什么时候使用deep Clone,这个主要看具体对象的域是什么性质的,基本型别还是reference variable + +调用Clone()方法的对象所属的类(Class)必须implements Clonable接口,否则在调用Clone方法的时候 会抛出CloneNotSupportedException + +推荐: [浅克隆(ShallowClone)和深克隆(DeepClone)区别以及实现](https://blog.csdn.net/qiaziliping/article/details/105566397) + +### 2 toString()方法 + +```java +public String toString() { + return getClass().getName() + "@" + Integer.toHexString(hashCode()); +} +``` + +Object 类的 toString 方法返回一个字符串,该字符串由类名(对象是该类的一个实例)、at 标记符“@” 和此对象[哈希码](https://baike.baidu.com/item/%E5%93%88%E5%B8%8C%E7%A0%81/5035512?fr=aladdin)的无符号十六进制表示组成。 + +该方法用得比较多,**一般子类都有覆盖。** + +```java +public static void main(String[] args){ + Object o1 = new Object(); + System.out.println(o1.toString()); +} +``` + +### 3 getClass()方法 + +```java +public final native Class getClass(); +``` + +返回次Object的运行时类类型。 + +不可重写,要调用的话,一般和getName()联合使用,如getClass().getName(); + +```java +public static void main(String[] args) { + Object o = new Object(); + System.out.println(o.getClass()); + //class java.lang.Object +} +``` + +### 4 finalize()方法 + +```java +protected void finalize() throws Throwable { } +``` + +该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。 + +Java允许在类中定义一个名为finalize()的方法。它的工作原理是:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法。并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。 + +关于垃圾回收,有三点需要记住: + +1、对象可能不被垃圾回收。只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。 + +2、垃圾回收并不等于“析构”。 + +​ 科普:析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。 + +3、垃圾回收只与内存有关。使用垃圾回收的唯一原因是为了回收程序不再使用的内存。 + +**finalize()的用途:** + +无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。 + +这就将对finalize()的需求限制到一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。不过这种情况一般发生在使用“本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式。 + +### 5 equals()方法 + +```java +public boolean equals(Object obj) { + return (this == obj); +} +``` + +Object中的equals方法是直接判断this和obj本身的值是否相等,即用来判断调用equals的对象和形参 obj所引用的对象是否是同一对象 + +所谓同一对象就是指内存中同一块存储单元,如果this和obj指向的是同一块内存对象,则返回true,如果this和obj指向的不是同一块内存,则返回false。 + +注意:即便是内容完全相等的两块不同的内存对象,也返回false。 + +如果是同一块内存,则object中的equals方法返回true,如果是不同的内存,则返回false + +如果希望不同内存但相同内容的两个对象equals时返回true,则我们需要重写父类的equal方法 + +String类已经重写了object中的equals方法(这样就是比较内容是否相等了) + +**查看String类源码equals方法** + +```java +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + while (n-- != 0) { + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; +} +``` + +### 6 hashCode()方法 + +```java +public native int hashCode(); +``` + +返回该对象的哈希码值。 + +该方法用于哈希查找,可以减少在查找中使用equals的次数,重写了equals方法一般都要重写 hashCode方法。这个方法在一些具有哈希功能的Collection中用到。 + +一般必须满足obj1.equals(obj2)==true。可以推出obj1.hashCode() == obj2.hashCode(),但是 hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。 + +### 7 wait()方法 + +```java +public final void wait(long timeoutMillis, int nanos) throws InterruptedException { + if (timeoutMillis < 0) { + throw new IllegalArgumentException("timeoutMillis value is negative"); + } + + if (nanos < 0 || nanos > 999999) { + throw new IllegalArgumentException( + "nanosecond timeout value out of range"); + } + + if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) { + timeoutMillis++; + } + + wait(timeoutMillis); +} +``` + +可以看到有三种重载,wait什么意思呢? + +![image-20210329225829453](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210329225829453.png) + +方法中的异常: + +![image-20210329225858173](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210329225858173.png) + +wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。 + +wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔 + +如果在规定时间内没有获得锁就返回。 + +调用该方法后当前线程进入睡眠状态,直到以下事件发生。 + +(1)其他线程调用了该对象的notify方法。 +(2)其他线程调用了该对象的notifyAll方法。 +(3)其他线程调用了interrupt中断该线程。 +(4)时间间隔到了。 + +此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。 + +### 8 notify()方法 + +```java +public final native void notify(); +``` + +该方法唤醒在该对象上等待的某个线程。 + +```java +public final native void notifyAll(); +``` + +该方法唤醒在该对象上等待的所有线程。 + +## 包装类 + +### 1、包装类介绍 + +虽然 Java 语言是典型的面向对象编程语言,但其中的八种基本数据类型并不支持面向对象编程,基本类型的数据不具备“对象”的特性——不携带属性、没有方法可调用。 沿用它们只是为了迎合人类根深蒂固的习惯,并的确能简单、有效地进行常规数据处理。 + +这种借助于非面向对象技术的做法有时也会带来不便,比如引用类型数据均继承了 Object 类的特性,要转换为 String 类型(经常有这种需要)时只要简单调用 Object 类中定义的toString()即可,而基本数据类型转换为 String 类型则要麻烦得多。为解决此类问题 ,Java为每种基本数据类型分别设计了对应的类,称之为包装类(Wrapper Classes),也有教材称为外覆类或数据类型类。 + +| 基本数据类型 | 对应的包装类 | +| ------------ | ------------ | +| byte | Byte | +| short | Short | +| int | Integer | +| long | Long | +| char | Character | +| float | Float | +| double | Double | +| boolean | Boolean | + +每个包装类的对象可以封装一个相应的基本类型的数据,并提供了其它一些有用的方法。包装类对象一 经创建,其内容(所封装的基本类型数据值)不可改变。 + +基本类型和对应的包装类可以相互装换: + +- 由基本类型向对应的包装类转换称为装箱,例如把 int 包装成 Integer 类的对象; +- 包装类向对应的基本类型转换称为拆箱,例如把 Integer 类的对象重新简化为 int。 + +### 2、包装类的应用 + +**1、 实现 int 和 Integer 的相互转换** + +可以通过 Integer 类的构造方法将 int 装箱,通过 Integer 类的 intValue 方法将 Integer 拆箱。 + +```java +public static void main(String[] args) { + int m = 500; + Integer obj = new Integer(m); // 手动装箱 + int n = obj.intValue(); // 手动拆箱 + System.out.println("n = " + n); + + Integer obj1 = new Integer(500); + System.out.println("obj 等价于 obj1?" + obj.equals(obj1)); +} +``` + +**2、 将字符串转换为整数** + +Integer 类有一个静态的 paseInt() 方法,可以将字符串转换为整数,语法为: + +```java +parseInt(String s, int radix); +//s 为要转换的字符串,radix 为进制,可选,默认为十进制。 +``` + +下面的代码将会告诉你什么样的字符串可以转换为整数: + +```java +public static void main(String[] args) { + String[] strs = {"123", "123abc", "abc123", "abcxyz"}; + for (String str : strs) { + try { + int m = Integer.parseInt(str, 10); + System.out.println(str + " 可以转换为整数 " + m); + } catch (Exception e) { + System.out.println(str + " 无法转换为整数"); + } + } +} +``` + +结果: + +> 123 可以转换为整数 123 +> 123abc 无法转换为整数 +> abc123 无法转换为整数 +> abcxyz 无法转换为整数 + +### 3、将整数转换为字符串 + +Integer 类有一个静态的 toString() 方法,可以将整数转换为字符串。或者直接在整数后面加空字符串! + +```java +public static void main(String[] args) { + int m = 500; + String s = Integer.toString(m); + + String s2 = m +""; + System.out.println("s = " + s); +} +``` + +### 3、自动拆箱和装箱 + +上面的例子都需要手动实例化一个包装类,称为手动拆箱装箱。Java 1.5(5.0) 之前必须手动拆箱装箱。 + +Java 1.5 之后可以自动拆箱装箱,也就是在进行基本数据类型和对应的包装类转换时,系统将自动进行,这将大大方便程序员的代码书写。 + +```java +public static void main(String[] args) { + int m = 500; + Integer obj = m; // 自动装箱 + int n = obj; // 自动拆箱 + System.out.println("n = " + n); + Integer obj1 = 500; + System.out.println("obj 等价于 obj1?" + obj.equals(obj1)); +} +//结果: +// n = 500 +// obj 等价于 obj1?true +``` + +自动装箱与拆箱的功能事实上是编译器来帮您的忙,编译器在编译时期依您所编写的语法,决定是否进行装箱或拆箱动作。例如: + +```java +Integer i = 100; +//相当于编译器自动为您作以下的语法编译: +Integer i = new Integer(100); +``` + +所以自动装箱与拆箱的功能是所谓的“编译器蜜糖”(Compiler Sugar),虽然使用这个功能很方便,但在程序运行阶段你要了解Java的语义。例如下面的程序是可以通过编译的: + +```java +Integer i = null; +int j = i; +``` + +这样的语法在编译时期是合法的,但是在运行时期会有错误,因为这种写法相当于: + +```java +Integer i = null; +int j = i.intValue(); +``` + +null表示i 没有参考至任何的对象实体,它可以合法地指定给对象参考名称。由于实际上 i 并没有参考至任何的对象,所以也就不可能操作intValue()方法,这样上面的写法在运行时会出现NullPointerException 错误。 + +自动拆箱装箱是常用的一个功能,需要重点掌握。 + +一般地,当需要使用数字的时候,我们通常使用内置数据类型,如:byte、int、long、double 等。然而,在实际开发过程中,我们经常会遇到需要使用对象,而不是内置数据类型的情形。为了解决这个问题,Java 语言为每一个内置数据类型提供了对应的包装类。 + +所有的包装类(Integer、Long、Byte、Double、Float、Short)都是抽象类 Number 的子类。 + +## Math类 + +Java 的 Math 包含了用于执行基本数学运算的属性和方法,如初等指数、对数、平方根和三角函数。 + +Math 的方法都被定义为 static 形式,通过 Math 类可以在主函数中直接调用 + +**【演示:查看Math类的源码】** + +```java +public final class Math{ + //数学方法 +} +``` + +【常用值与函数】 + +Math.PI 记录的圆周率 +Math.E 记录e的常量 + +Math中还有一些类似的常量,都是一些工程数学常用量。 + +Math.abs 求绝对值 +Math.sin 正弦函数 Math.asin 反正弦函数 +Math.cos 余弦函数 Math.acos 反余弦函数 +Math.tan 正切函数 Math.atan 反正切函数 Math.atan2 商的反正切函数 +Math.toDegrees 弧度转化为角度 Math.toRadians 角度转化为弧度 +Math.ceil 得到不小于某数的最大整数 +Math.floor 得到不大于某数的最大整数 +Math.IEEEremainder 求余 +Math.max 求两数中最大 +Math.min 求两数中最小 +Math.sqrt 求开方 +Math.pow 求某数的任意次方, 抛出ArithmeticException处理溢出异常 + +Math.exp 求e的任意次方 +Math.log10 以10为底的对数 +Math.log 自然对数 +Math.rint 求距离某数最近的整数(可能比某数大,也可能比它小) +Math.round 同上,返回int型或者long型(上一个函数返回double型) +Math.random 返回0,1之间的一个随机数 + +```java +public static void main(String[] args) { + /** + *Math.sqrt()//计算平方根 + *Math.cbrt()//计算立方根 + *Math.pow(a, b)//计算a的b次方 + *Math.max( , );//计算最大值 + *Math.min( , );//计算最小值 + */ + System.out.println(Math.sqrt(16)); //4.0 + System.out.println(Math.cbrt(8)); //2.0 + System.out.println(Math.pow(3, 2)); //9.0 + System.out.println(Math.max(2.3, 4.5));//4.5 + System.out.println(Math.min(2.3, 4.5));//2.3 + /** + * abs求绝对值 + */ + System.out.println(Math.abs(-10.4)); //10.4 + System.out.println(Math.abs(10.1)); //10.1 + /** + * ceil天花板的意思,就是返回大的值 + */ + System.out.println(Math.ceil(-10.1)); //-10.0 + System.out.println(Math.ceil(10.7)); //11.0 + System.out.println(Math.ceil(-0.7)); //-0.0 + System.out.println(Math.ceil(0.0)); //0.0 + System.out.println(Math.ceil(-0.0)); //-0.0 + System.out.println(Math.ceil(-1.7)); //-1.0 + /** + * floor地板的意思,就是返回小的值 + */ + System.out.println(Math.floor(-10.1)); //-11.0 + System.out.println(Math.floor(10.7)); //10.0 + System.out.println(Math.floor(-0.7)); //-1.0 + System.out.println(Math.floor(0.0)); //0.0 + System.out.println(Math.floor(-0.0)); //-0.0 + /** + * random 取得一个大于或者等于0.0小于不等于1.0的随机数 [0,1) + */ + System.out.println(Math.random()); //小于1大于0的double类型的数 + System.out.println(Math.random() + 1);//大于1小于2的double类型的数 + /** + * rint 四舍五入,返回double值 + * 注意.5的时候会取偶数 异常的尴尬=。= + */ + System.out.println(Math.rint(10.1)); //10.0 + System.out.println(Math.rint(10.7)); //11.0 + System.out.println(Math.rint(11.5)); //12.0 + System.out.println(Math.rint(10.5)); //10.0 + System.out.println(Math.rint(10.51)); //11.0 + System.out.println(Math.rint(-10.5)); //-10.0 + System.out.println(Math.rint(-11.5)); //-12.0 + System.out.println(Math.rint(-10.51)); //-11.0 + System.out.println(Math.rint(-10.6)); //-11.0 + System.out.println(Math.rint(-10.2)); //-10.0 + /** + * round 四舍五入,float时返回int值,double时返回long值 + */ + System.out.println(Math.round(10.1)); //10 + System.out.println(Math.round(10.7)); //11 + System.out.println(Math.round(10.5)); //11 + System.out.println(Math.round(10.51)); //11 + System.out.println(Math.round(-10.5)); //-10 + System.out.println(Math.round(-10.51)); //-11 + System.out.println(Math.round(-10.6)); //-11 + System.out.println(Math.round(-10.2)); //-10 +} +``` + +## Random类 + +Java中存在着两种Random函数: + + **1、java.lang.Math.Random;** + +调用这个Math.Random()函数能够返回带正号的double值,该值大于等于0.0且小于1.0,即取值范围是 [0.0,1.0)的左闭右开区间,返回值是一个伪随机选择的数,在该范围内(近似)均匀分布。例子如下: + +```java +public static void main(String[] args) { + // 结果是个double类型的值,区间为[0.0,1.0) + System.out.println("Math.random()=" + Math.random()); + int num = (int) (Math.random() * 3); + // 注意不要写成(int)Math.random()*3,这个结果为0或1,因为先执行了强制转换 + System.out.println("num=" + num); +} +``` + + + +**2、java.util.Random** + +下面是Random()的两种构造方法: + + Random():创建一个新的随机数生成器 + Random(long seed):使用单个 long 种子创建一个新的随机数生成器。 + +你在创建一个Random对象的时候可以给定任意一个合法的种子数,种子数只是随机算法的起源数字,和生成的随机数的区间没有任何关系。 + +如下面的Java代码: + +【演示一】 + +在没带参数构造函数生成的Random对象的种子是当前系统时间的毫秒数。 + +rand.nextInt(100)中的100是随机数的上限,产生的随机数为0-100的整数,不包括100。 + +```java +public static void main(String[] args) { + Random rand = new Random(); + int i = rand.nextInt(100); + System.out.println(i); +} +``` + +【演示二】 + +对于种子相同的Random对象,多次执行后生成的随机数序列是一样的。 + +```java +public static void main(String[] args) { + Random ran1 = new Random(25); + System.out.println("使用种子为25的Random对象生成[0,100)内随机整数序列: "); + for (int i = 0; i < 10; i++) { + System.out.print(ran1.nextInt(100) + " "); + } + System.out.println(); +} +``` + +【方法摘要】 + +1. protected int next(int bits):生成下一个伪随机数。 +2. boolean nextBoolean():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 boolean值。 +3. void nextBytes(byte[] bytes):生成随机字节并将其置于用户提供的 byte 数组中。 +4. double nextDouble():返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0和1.0之间 均匀分布的 double值。 +5. float nextFloat():返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0和1.0之间均匀分布float值。 +6. double nextGaussian():返回下一个伪随机数,它是取自此随机数生成器序列的、呈高斯(“正态”)分布的double值,其平均值是0.0标准差是1.0。 +7. int nextInt():返回下一个伪随机数,它是此随机数生成器的序列中均匀分布的 int 值。 +8. int nextInt(int n):返回一个伪随机数,它是取自此随机数生成器序列的、在(包括和指定值(不包括)之间均匀分布的int值。 +9. long nextLong():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 long 值。 +10. void setSeed(long seed):使用单个 long 种子设置此随机数生成器的种子。 + +【例子】 + +1. 生成[0,1.0)区间的小数:double d1 = r.nextDouble(); +2. 生成[0,5.0)区间的小数:double d2 = r.nextDouble() * 5; +3. 生成[1,2.5)区间的小数:double d3 = r.nextDouble() * 1.5 + 1; +4. 生成[0,10)区间的整数:int n2 = r.nextInt(10); + +## 日期时间类 + +### 1 Date类 + +java.util 包提供了 Date 类来封装当前的日期和时间。 + +Date 类提供两个构造函数来实例化 Date 对象。 + +第一个构造函数使用当前日期和时间来初始化对象。 + +```java +Date() +``` + +第二个构造函数接收一个参数,该参数是从1970年1月1日起的毫秒数。 + +```java +Date(long millisec) +``` + +Date对象创建以后,可以调用下面的方法。 + +| 序号 | 方法和描述 | +| :--- | :----------------------------------------------------------- | +| 1 | **boolean after(Date date)** 若当调用此方法的Date对象在指定日期之后返回true,否则返回false。 | +| 2 | **boolean before(Date date)** 若当调用此方法的Date对象在指定日期之前返回true,否则返回false。 | +| 3 | **Object clone( )** 返回此对象的副本。 | +| 4 | **int compareTo(Date date)** 比较当调用此方法的Date对象和指定日期。两者相等时候返回0。调用对象在指定日期之前则返回负数。调用对象在指定日期之后则返回正数。 | +| 5 | **int compareTo(Object obj)** 若obj是Date类型则操作等同于compareTo(Date) 。否则它抛出ClassCastException。 | +| 6 | **boolean equals(Object date)** 当调用此方法的Date对象和指定日期相等时候返回true,否则返回false。 | +| 7 | **long getTime( )** 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。 | +| 8 | **int hashCode( )** 返回此对象的哈希码值。 | +| 9 | **void setTime(long time)** 用自1970年1月1日00:00:00 GMT以后time毫秒数设置时间和日期。 | +| 10 | **String toString( )** 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat)。 | + +【演示:获取当前日期时间】 + +Java中获取当前日期和时间很简单,使用 Date 对象的 toString() 方法来打印当前日期和时间 + +```java +public static void main(String args[]) { + // 初始化 Date 对象 + Date date = new Date(); + + // 使用 toString() 函数显示日期时间 + System.out.println(date.toString()); + //Tue Mar 30 10:24:19 CST 2021 +} +``` + +【演示:日期比较】 + +使用 getTime() 方法获取两个日期(自1970年1月1日经历的毫秒数值),然后比较这两个值。 + +```java +public static void main(String[] args) { + // 初始化 Date 对象 + Date date = new Date(); + + long time = date.getTime(); + long time2 = date.getTime(); + System.out.println(time==time2);//true +} +``` + +使用方法 before(),after() 和 equals()。例如,一个月的12号比18号早,则 new Date(99, 2, 12).before(new Date (99, 2, 18)) 返回true。 + +```java +public static void main(String[] args) { + boolean before = new Date(99, 01, 05).before(new Date(99, 11, 16)); + System.out.println(before); +} +``` + +### 2、SimpleDateFormat + +格式化日期 + +SimpleDateFormat 是一个以语言环境敏感的方式来格式化和分析日期的类。SimpleDateFormat 允许你选择任何用户自定义日期时间格式来运行。例如: + +```java +public static void main(String args[]) { + Date dNow = new Date( ); + SimpleDateFormat ft = new SimpleDateFormat ("yyyy-MM-dd hh:mm:ss"); + System.out.println("当前时间为: " + ft.format(dNow)); +} +``` + +其中 yyyy 是完整的公元年,MM 是月份,dd 是日期,HH:mm:ss 是时、分、秒。 + +注意:有的格式大写,有的格式小写,例如 MM 是月份,mm 是分;HH 是 24 小时制,而 hh 是 12 小时制。 + +时间模式字符串用来指定时间格式。在此模式中,所有的 ASCII 字母被保留为模式字母,定义如下: + +| **字母** | **描述** | **示例** | +| :------- | :----------------------- | :---------------------- | +| G | 纪元标记 | AD | +| y | 四位年份 | 2001 | +| M | 月份 | July or 07 | +| d | 一个月的日期 | 10 | +| h | A.M./P.M. (1~12)格式小时 | 12 | +| H | 一天中的小时 (0~23) | 22 | +| m | 分钟数 | 30 | +| s | 秒数 | 55 | +| S | 毫秒数 | 234 | +| E | 星期几 | Tuesday | +| D | 一年中的日子 | 360 | +| F | 一个月中第几周的周几 | 2 (second Wed. in July) | +| w | 一年中第几周 | 40 | +| W | 一个月中第几周 | 1 | +| a | A.M./P.M. 标记 | PM | +| k | 一天中的小时(1~24) | 24 | +| K | A.M./P.M. (0~11)格式小时 | 10 | +| z | 时区 | Eastern Standard Time | +| ' | 文字定界符 | Delimiter | +| " | 单引号 | ` | + +【演示:使用printf格式化日期】 + +Java 格式化输出 printf 例子:https://www.runoob.com/w3cnote/java-printf-formate-demo.html + +printf 方法可以很轻松地格式化时间和日期。使用两个字母格式,它以 %t 开头并且以下面表格中的一个字母结尾。 + +```java +public static void main(String[] args) { + // 初始化 Date 对象 + Date date = new Date(); + //c的使用 + System.out.printf("全部日期和时间信息:%tc%n",date); + //f的使用 + System.out.printf("年-月-日格式:%tF%n",date); + //d的使用 + System.out.printf("月/日/年格式:%tD%n",date); + //r的使用 + System.out.printf("HH:MM:SS PM格式(12时制):%tr%n",date); + //t的使用 + System.out.printf("HH:MM:SS格式(24时制):%tT%n",date); + //R的使用 + System.out.printf("HH:MM格式(24时制):%tR",date); +} +``` + +结果: + +>全部日期和时间信息:周二 3月 30 10:37:02 CST 2021 +>年-月-日格式:2021-03-30 +>月/日/年格式:03/30/21 +>HH:MM:SS PM格式(12时制):10:37:02 上午 +>HH:MM:SS格式(24时制):10:37:02 +>HH:MM格式(24时制):10:37 + +【时间休眠:休眠(sleep)】 + +sleep()使当前线程进入停滞状态(阻塞当前线程),让出CPU的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会。 + +你可以让程序休眠一毫秒的时间或者到您的计算机的寿命长的任意段时间。例如,下面的程序会休眠3秒: + +```java +public static void main(String args[]) { + try { + System.out.println(new Date( ) + "\n"); + Thread.sleep(1000*3); // 休眠3秒 + System.out.println(new Date( ) + "\n"); + } catch (Exception e) { + System.out.println("Got an exception!"); + } +} +``` + +### 3、Calendar类 + +我们现在已经能够格式化并创建一个日期对象了,但是我们如何才能设置和获取日期数据的特定部分呢,比如说小时,日,或者分钟? 我们又如何在日期的这些部分加上或者减去值呢? 答案是使用Calendar类。Date中有很多方法都已经废弃了! + +Calendar类的功能要比Date类强大很多,而且在实现方式上也比Date类要复杂一些。 + +Calendar类是一个抽象类,在实际使用时实现特定的子类的对象,创建对象的过程对程序员来说是透明的,只需要使用getInstance方法创建即可。 + +**创建一个代表系统当前日期的Calendar对象** + +```java +public static void main(String args[]) { + Calendar c = Calendar.getInstance();//默认是当前日期 + System.out.println(c); +} +``` + +输出: +> java.util.GregorianCalendar[time=1617072097924,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2021,MONTH=2,WEEK_OF_YEAR=14,WEEK_OF_MONTH=5,DAY_OF_MONTH=30,DAY_OF_YEAR=89,DAY_OF_WEEK=3,DAY_OF_WEEK_IN_MONTH=5,AM_PM=0,HOUR=10,HOUR_OF_DAY=10,MINUTE=41,SECOND=37,MILLISECOND=924,ZONE_OFFSET=28800000,DST_OFFSET=0] + +**创建一个指定日期的Calendar对象** + +使用Calendar类代表特定的时间,需要首先创建一个Calendar的对象,然后再设定该对象中的年月日参数来完成。 + +```java +//创建一个代表2019年4月27日的Calendar对象 +Calendar c1 = Calendar.getInstance(); +c1.set(2019, 4 - 1, 27); +``` + +**Calendar类对象字段类型** + +Calendar类中用以下这些常量表示不同的意义,jdk内的很多类其实都是采用的这种思想 + +| 常量 | 描述 | +| :-------------------- | :----------------------------- | +| Calendar.YEAR | 年份 | +| Calendar.MONTH | 月份 | +| Calendar.DATE | 日期 | +| Calendar.DAY_OF_MONTH | 日期,和上面的字段意义完全相同 | +| Calendar.HOUR | 12小时制的小时 | +| Calendar.HOUR_OF_DAY | 24小时制的小时 | +| Calendar.MINUTE | 分钟 | +| Calendar.SECOND | 秒 | +| Calendar.DAY_OF_WEEK | 星期几 | + +```java +// 获得年份 +int year = c1.get(Calendar.YEAR); +// 获得月份 +int month = c1.get(Calendar.MONTH) + 1; +// 获得日期 +int date = c1.get(Calendar.DATE); +// 获得小时 +int hour = c1.get(Calendar.HOUR_OF_DAY); +// 获得分钟 +int minute = c1.get(Calendar.MINUTE); +// 获得秒 +int second = c1.get(Calendar.SECOND); +// 获得星期几(注意(这个与Date类是不同的):1代表星期日、2代表星期1、3代表星期二,以此类推) +int day = c1.get(Calendar.DAY_OF_WEEK); +``` + +设置完整日期 + +```java +Calendar c1 = Calendar.getInstance(); +c1.set(2009, 6 - 1, 12);//把Calendar对象c1的年月日分别设这为:2009、6、12 +``` + +设置某个字段 + +```java +c1.set(Calendar.DATE,10); +c1.set(Calendar.YEAR,2008); +//其他字段属性set的意义以此类推 +``` + +add设置 + +```java +//把c1对象的日期加上10,也就是c1也就表示为10天后的日期,其它所有的数值会被重新计算 +c1.add(Calendar.DATE, 10); +//把c1对象的日期减去10,也就是c1也就表示为10天前的日期,其它所有的数值会被重新计算 +c1.add(Calendar.DATE, -10); +``` + +【演示:GregorianCalendar】 + +```java +public static void main(String[] args) { + String months[] = { + "Jan", "Feb", "Mar", "Apr", + "May", "Jun", "Jul", "Aug", + "Sep", "Oct", "Nov", "Dec"}; + + int year; + // 初始化 Gregorian 日历 + // 使用当前时间和日期 + // 默认为本地时间和时区 + GregorianCalendar gcalendar = new GregorianCalendar(); + // 显示当前时间和日期的信息 + System.out.print("Date: "); + System.out.print(months[gcalendar.get(Calendar.MONTH)]); + System.out.print(" " + gcalendar.get(Calendar.DATE) + " "); + System.out.println(year = gcalendar.get(Calendar.YEAR)); + System.out.print("Time: "); + System.out.print(gcalendar.get(Calendar.HOUR) + ":"); + System.out.print(gcalendar.get(Calendar.MINUTE) + ":"); + System.out.println(gcalendar.get(Calendar.SECOND)); + + // 测试当前年份是否为闰年 + if (gcalendar.isLeapYear(year)) { + System.out.println("当前年份是闰年"); + } else { + System.out.println("当前年份不是闰年"); + } + +} +``` + +输出: + +> Date: Mar 30 2021 +> Time: 10:49:35 +> 当前年份不是闰年 + +**注意:Calender的月份是从0开始的,但日期和年份是从1开始的** + +【演示】 + +```java +public static void main(String[] args) { + Calendar c1 = Calendar.getInstance(); + c1.set(2017, 1, 1); + System.out.println(c1.get(Calendar.YEAR) + + "-" + c1.get(Calendar.MONTH) + + "-" + c1.get(Calendar.DATE)); + c1.set(2017, 1, 0); + System.out.println(c1.get(Calendar.YEAR) + + "-" + c1.get(Calendar.MONTH) + + "-" + c1.get(Calendar.DATE)); +} +/* +输出 +2017-1-1 +2017-0-31 + /* +``` + +可见,将日期设为0以后,月份变成了上个月,但月份可以为0,把月份改为2试试: + +```java +public static void main(String[] args) { + Calendar c1 = Calendar.getInstance(); + c1.set(2017, 2, 1); + System.out.println(c1.get(Calendar.YEAR) + + "-" + c1.get(Calendar.MONTH) + + "-" + c1.get(Calendar.DATE)); + c1.set(2017, 2, 0); + System.out.println(c1.get(Calendar.YEAR) + + "-" + c1.get(Calendar.MONTH) + + "-" + c1.get(Calendar.DATE)); +} +/* +输出 +2017-2-1 +2017-1-28 +*/ +``` + +可以看到上个月的最后一天是28号,所以Calendar.MONTH为1的时候是2月 。 + +## String类 + +### 1、String概述 + +在API中是这样描述: + + String 类代表字符串。Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例实现。 字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的, 所以可以共享。 + +【演示:查看String源码】 + +```java +public final class String +implements java.io.Serializable, Comparable, CharSequence { + +} +``` + +【String的成员变量】 + +```java +//String的属性值 +private final char value[]; + +//数组被使用的开始位置 +private final int offset; + +//String中元素的个数 +private final int count; + +//String类型的hash值 +private int hash; // Default to 0 + +private static final long serialVersionUID = -6849794470754667710L; +private static final ObjectStreamField[] serialPersistentFields = +new ObjectStreamField[0]; +``` + +从源码看出String底层使用一个字符数组来维护的。 + +成员变量可以知道String类的值是final类型的,不能被改变的,所以只要一个值改变就会生成一个新的 String类型对象,存储String数据也不一定从数组的第0个元素开始的,而是从offset所指的元素开始。 + +【String的构造方法】 + +```java +String() +//初始化一个新创建的 String 对象,使其表示一个空字符序列。 + +String(byte[] bytes) +//通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。 + +String(byte[] bytes, Charset charset) +//通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。 + +String(byte[] bytes, int offset, int length) +//通过使用平台的默认字符集解码指定的 byte 子数组,构造一个新的 String。 + +String(byte[] bytes, int offset, int length, Charset charset) +//通过使用指定的 charset 解码指定的 byte 子数组,构造一个新的 String。 + +String(byte[] bytes, int offset, int length, String charsetName) +//通过使用指定的字符集解码指定的 byte 子数组,构造一个新的 String。 + +String(byte[] bytes, String charsetName) +//通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。 + +String(char[] value) +//分配一个新的 String,使其表示字符数组参数中当前包含的字符序列。 + +String(char[] value, int offset, int count) +//分配一个新的 String,它包含取自字符数组参数一个子数组的字符。 + +String(int[] codePoints, int offset, int count) +//分配一个新的 String,它包含 Unicode 代码点数组参数一个子数组的字符。 + +String(String original) +//初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。 + +String(StringBuffer buffer) +//分配一个新的字符串,它包含字符串缓冲区参数中当前包含的字符序列。 + +String(StringBuilder builder) +//分配一个新的字符串,它包含字符串生成器参数中当前包含的字符序列。 +``` + +### 2、创建字符串对象方式 + +直接赋值方式创建对象是在方法区的常量池 + +```java +String str="hello";//直接赋值的方式 +``` + +通过构造方法创建字符串对象是在堆内存 + +```java +String str=new String("hello");//实例化的方式 +``` + +【两种实例化方式的比较】 + + 编写代码比较 + +```java +public static void main(String[] args) { + String str1 = "Lance"; + String str2 = new String("Lance"); + String str3 = str2; //引用传递,str3直接指向st2的堆内存地址 + String str4 = "Lance"; + /** + * ==: + * 基本数据类型:比较的是基本数据类型的值是否相同 + * 引用数据类型:比较的是引用数据类型的地址值是否相同 + * 所以在这里的话:String类对象==比较,比较的是地址,而不是内容 + */ + System.out.println(str1 == str2);//false + System.out.println(str1 == str3);//false + System.out.println(str3 == str2);//true + System.out.println(str1 == str4);//true +} +``` + +内存图分析 + +![image-20210330110521687](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210330110521687.png) + +可能这里还是不够明显,构造方法实例化方式的内存图:String str = new String("Hello"); + +![image-20210330110555759](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210330110555759.png) + +当我们再一次的new一个String对象时: + +![image-20210330110629270](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210330110629270.png) + +【字符串常量池】 + +在字符串中,如果采用直接赋值的方式(String str="Lance")进行对象的实例化,则会将匿名对象 “Lance”放入对象池,每当下一次对不同的对象进行直接赋值的时候会直接利用池中原有的匿名对象,我们可以用对象手工入池; + +```java +public static void main(String args[]){ + String str = new String("Lance").intern();//对匿名对象进行手工入池操作 + String str1 = "Lance"; + System.out.println(str==str1);//true +} +``` + +【两种实例化方式的区别】 + +1. 直接赋值(String str = "hello") + + 只开辟一块堆内存空间,并且会自动入池,不会产生垃圾。 + +2. 构造方法(String str= new String("hello");) + + 会开辟两块堆内存空间,其中一块堆内存会变成垃圾被系统回收,而且不能够自动入池,需要通过public String intern();方法进行手工入池。 + +3. 在开发的过程中不会采用构造方法进行字符串的实例化。 + +【避免空指向】 + +首先了解: == 和public boolean equals()比较字符串的区别 + +==在对字符串比较的时候,对比的是内存地址,而equals比较的是字符串内容,在开发的过程中, equals()通过接受参数,可以避免空指向。 + +```java +String str = null; +if(str.equals("hello")){//此时会出现空指向异常 + +} + +if("hello".equals(str)){//此时equals会处理null值,可以避免空指向异常 + +} +``` + +String类对象一旦声明则不可以改变;而改变的只是地址,原来的字符串还是存在的,并且产生垃圾 + +![image-20210330111048863](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210330111048863.png) + +### 3、String常用的方法 + +![image-20210330111326341](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210330111326341.png) + +#### String的判断 + +> boolean equals(Object obj):比较字符串的内容是否相同 +> boolean equalsIgnoreCase(String str): 比较字符串的内容是否相同,忽略大小写 +> boolean startsWith(String str): 判断字符串对象是否以指定的str开头 +> boolean endsWith(String str): 判断字符串对象是否以指定的str结尾 + +演示: + +```java +public static void main(String[] args) { + // 创建字符串对象 + String s1 = "hello"; + String s2 = "hello"; + String s3 = "Hello"; + + // boolean equals(Object obj):比较字符串的内容是否相同 + System.out.println(s1.equals(s2)); //true + System.out.println(s1.equals(s3)); //false + System.out.println("-----------"); + + // boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写 + System.out.println(s1.equalsIgnoreCase(s2)); //true + System.out.println(s1.equalsIgnoreCase(s3)); //true + System.out.println("-----------"); + + // boolean startsWith(String str):判断字符串对象是否以指定的str开头 + System.out.println(s1.startsWith("he")); //true + System.out.println(s1.startsWith("ll")); //false +} +``` + +#### String的截取 + +> int length():获取字符串的长度,其实也就是字符个数 +> char charAt(int index):获取指定索引处的字符 +> int indexOf(String str):获取str在字符串对象中第一次出现的索引 +> String substring(int start):从start开始截取字符串 +> String substring(int start,int end):从start开始,到end结束截取字符串。包括start,不包括end + +演示 + +```java +public static void main(String args[]) { + // 创建字符串对象 + String s = "helloworld"; + // int length():获取字符串的长度,其实也就是字符个数 + System.out.println(s.length()); //10 + System.out.println("--------"); + // char charAt(int index):获取指定索引处的字符 + System.out.println(s.charAt(0)); //h + System.out.println(s.charAt(1)); //e + System.out.println("--------"); + // int indexOf(String str):获取str在字符串对象中第一次出现的索引 + System.out.println(s.indexOf("l")); //2 + System.out.println(s.indexOf("owo")); //4 + System.out.println(s.indexOf("ak")); //-1 + System.out.println("--------"); + // String substring(int start):从start开始截取字符串 + System.out.println(s.substring(0)); //helloworld + System.out.println(s.substring(5)); //world + System.out.println("--------"); + // String substring(int start,int end):从start开始,到end结束截取字符串 + System.out.println(s.substring(0, s.length())); //helloworld + System.out.println(s.substring(3, 8)); //lowor +} +``` + +#### String的转换 + +> char[] toCharArray():把字符串转换为字符数组 +> String toLowerCase():把字符串转换为小写字符串 +> String toUpperCase():把字符串转换为大写字符串 + +演示 + +```java +public static void main(String args[]) { + // 创建字符串对象 + String s = "abcde"; + + // char[] toCharArray():把字符串转换为字符数组 + char[] chs = s.toCharArray(); + for (int x = 0; x < chs.length; x++) { + System.out.println(chs[x]); + } + + System.out.println("-----------"); + + // String toLowerCase():把字符串转换为小写字符串 + System.out.println("HelloWorld".toLowerCase()); + // String toUpperCase():把字符串转换为大写字符串 + System.out.println("HelloWorld".toUpperCase()); +} +``` + +#### 其他方法 + +> 去除字符串两端空格:String trim() +> +> 按照指定符号分割字符串:String[] split(String str) + +```java +public static void main(String args[]) { + // 创建字符串对象 + String s1 = "helloworld"; + String s2 = " helloworld "; + String s3 = " hello world "; + System.out.println("---" + s1 + "---"); + System.out.println("---" + s1.trim() + "---"); + System.out.println("---" + s2 + "---"); + System.out.println("---" + s2.trim() + "---"); + System.out.println("---" + s3 + "---"); + System.out.println("---" + s3.trim() + "---"); + System.out.println("-------------------"); + + // String[] split(String str) + // 创建字符串对象 + String s4 = "aa,bb,cc"; + String[] strArray = s4.split(","); + for (int x = 0; x < strArray.length; x++) { + System.out.println(strArray[x]); + } +} +``` + +### 4、String的不可变性 + +当我们去阅读源代码的时候,会发现有这样的一句话: + +> Strings are constant; their values cannot be changed after they are created. + +意思就是说:String是个常量,从一出生就注定不可变 + +我想大家应该就知道为什么String不可变了,String类被final修饰,官方注释说明创建后不能被改变,但 是为什么String要使用final修饰呢? + +了解一个经典的面试题 + +```java +public static void main(String[] args) { + String a = "abc"; + String b = "abc"; + String c = new String("abc"); + System.out.println(a==b); //true + System.out.println(a.equals(b)); //true + System.out.println(a==c); //false + System.out.println(a.equals(c)); //true +} +``` + +![image-20210330112025681](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210330112025681.png) + +因为String太过常用,JAVA类库的设计者在实现时做了个小小的变化,即采用了享元模式,每当生成一个新内容的字符串时,他们都被添加到一个共享池中,当第二次再次生成同样内容的字符串实例时,就共享此对象,而不是创建一个新对象,但是这样的做法仅仅适合于通过=符号进行的初始化。 + +需要说明一点的是,在object中,equals()是用来比较内存地址的,但是String重写了equals()方 法,用来比较内容的,即使是不同地址,只要内容一致,也会返回true,这也就是为什么a.equals(c)返回true的原因了。 + +**String不可变的好处** + +- 可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销。 +- 我们的程序中大量使用了String字符串,有可能是出于安全性考虑。 +- 大家都知道HashMap中key为String类型,如果可变将变的多么可怕。 +- 当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。 + +### 5、字符串常量池 + +字符串常量池概述: + +1、常量池表(Constant_Pool table) + +Class文件中存储所有常量(包括字符串)的table。这是Class文件中的内容,还不是运行时的内容,不要理解它是个池子,其实就是Class文件中的字节码指令。 + +2、运行时常量池(Runtime Constant Pool) + +JVM内存中方法区的一部分,这是运行时的内容。这部分内容(绝大部分)是随着JVM运行时候,从常量池转化而来,每个Class对应一个运行时常量池。上一句中说绝大部分是因为:除了 Class中常量池内 容,还可能包括动态生成并加入这里的内容。 + +3、字符串常量池(String Pool) + +这部分也在方法区中,但与Runtime Constant Pool不是一个概念,String Pool是JVM实例全局共享的,全局只有一个。JVM规范要求进入这里的String实例叫“被驻留的interned string”,各个JVM可以有不同的实现,HotSpot是设置了一个哈希表StringTable来引用堆中的字符串实例,被引用就是被驻留。 + +【亨元模式】 + +其实字符串常量池这个问题涉及到一个设计模式,叫“享元模式”,顾名思义 - - - > 共享元素模式 也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素 + +Java中String部分就是根据享元模式设计的,而那个存储元素的地方就叫做“字符串常量池 - String Pool” + +【详细分析】 + +```java +int x = 10; +String y = "hello"; +``` + +**①** 首先, `10 `和 `"hello" `会在经过javac(或者其他编译器)编译过后变为Class文件中 `constant_pool table` 的内容 + +**②** 当我们的程序运行时,也就是说JVM运行时,每个Class constant_pool table 中的内容会被加载到JVM内存中的方法区中各自Class的 Runtime Constant Pool。 + +**③** 一个没有被String Pool包含的Runtime Constant Pool中的字符串(这里是"hello")会被加入到 String Pool中(HosSpot使用hashtable引用方式),步骤如下: + +1. 在Java Heap(堆)中根据"hello"字面量create一个字符串对象 + +2. 将字面量"hello"与字符串对象的引用在hashtable中关联起来键 - 值 + + 形式是:"hello" = 对象的引用地址。 + +另外来说,当一个新的字符串出现在Runtime Constant Pool中时怎么判断需不需要在Java Heap中创建 新对象呢? + +策略是这样:会先去根据equals来比较Runtime Constant Pool中的这个字符串是否和String Pool中某一个是相等的(也就是找是否已经存在),如果有那么就不创建,直接使用其引用;反之,就如同上面的第三步。 + +如此,就实现了享元模式,提高的内存利用效率。 + +举例: + +> 使用String s = new String("hello");会创建几个对象 +> 答:会创建2个对象 +> +> 首先,出现了字面量"hello",那么去String Pool中查找是否有相同字符串存在,因为程序就这一行代码所以肯定没有,那么就在Java Heap中用字面量"hello"首先创建1个String对象。 +> +> 接着,new String("hello"),关键字new又在Java Heap中创建了1个对象,然后调用接收String 参数的构造器进行了初始化。最终s的引用是这个String对象. + +## StringBuilder和StringBuffer + +### 1、概述 + +【演示:查看源码及API文档】 + +```java +public final class StringBuilder + extends AbstractStringBuilder + implements java.io.Serializable, CharSequence{ + +} +``` + +StringBuilder 是一个可变的字符序列。它继承于AbstractStringBuilder,实现了CharSequence接口。 StringBuffer 也是继承于AbstractStringBuilder的子类; +但是,StringBuilder和StringBuffer不同,前者是非线程安全的,后者是线程安全的。 + +StringBuilder 和 CharSequence之间的关系图如下: + +![image-20210330113151662](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-常用类.assets/image-20210330113151662.png) + +【源码概览】 + +```java +package java.lang; +public final class StringBuilder + extends AbstractStringBuilder + implements java.io.Serializable, CharSequence { + + static final long serialVersionUID = 4383685877147921099L; + // 构造函数。默认的字符数组大小是16。 + public StringBuilder() { + super(16); + } + + // 构造函数。指定StringBuilder的字符数组大小是capacity。 + public StringBuilder(int capacity) { + super(capacity); + } + + // 构造函数。指定字符数组大小=str长度+15,且将str的值赋值到当前字符数组中。 + public StringBuilder(String str) { + super(str.length() + 16); + append(str); + } + + // 构造函数。指定字符数组大小=seq长度+15,且将seq的值赋值到当前字符数组中。 + public StringBuilder(CharSequence seq) { + this(seq.length() + 16); + append(seq); + } + + // 追加“对象obj对应的字符串”。String.valueOf(obj)实际上是调用obj.toString() + public StringBuilder append(Object obj) { + return append(String.valueOf(obj)); + } + + // 追加“str”。 + public StringBuilder append(String str) { + super.append(str); + return this; + } + + // 追加“sb的内容”。 + private StringBuilder append(StringBuilder sb) { + if (sb == null) + return append("null"); + int len = sb.length(); + int newcount = count + len; + if (newcount > value.length) + expandCapacity(newcount); + sb.getChars(0, len, value, count); + count = newcount; + return this; + } + + // 追加“sb的内容”。 + public StringBuilder append(StringBuffer sb) { + super.append(sb); + return this; + } + + // 追加“s的内容”。 + public StringBuilder append(CharSequence s) { + if (s == null) + s = "null"; + if (s instanceof String) + return this.append((String)s); + if (s instanceof StringBuffer) + return this.append((StringBuffer)s); + if (s instanceof StringBuilder) + return this.append((StringBuilder)s); + return this.append(s, 0, s.length()); + } + + // 追加“s从start(包括)到end(不包括)的内容”。 + public StringBuilder append(CharSequence s, int start, int end) { + super.append(s, start, end); + return this; + } + + // 追加“str字符数组对应的字符串” + public StringBuilder append(char[] str) { + super.append(str); + return this; + } + + // 追加“str从offset开始的内容,内容长度是len” + public StringBuilder append(char[] str, int offset, int len) { + super.append(str, offset, len); + return this; + } + + // 追加“b对应的字符串” + public StringBuilder append(boolean b) { + super.append(b); + return this; + } + + // 追加“c” + public StringBuilder append(char c) { + super.append(c); + return this; + } + + // 追加“i” + public StringBuilder append(int i) { + super.append(i); + return this; + } + + // 追加“lng” + public StringBuilder append(long lng) { + super.append(lng); + return this; + } + + // 追加“f” + public StringBuilder append(float f) { + super.append(f); + return this; + } + + // 追加“d” + public StringBuilder append(double d) { + super.append(d); + return this; + } + + // 追加“codePoint” + public StringBuilder appendCodePoint(int codePoint) { + super.appendCodePoint(codePoint); + return this; + } + + // 删除“从start(包括)到end的内容” + public StringBuilder delete(int start, int end) { + super.delete(start, end); + return this; + } + + // 删除“位置index的内容” + public StringBuilder deleteCharAt(int index) { + super.deleteCharAt(index); + return this; + } + + // “用str替换StringBuilder中从start(包括)到end(不包括)的内容” + public StringBuilder replace(int start, int end, String str) { + super.replace(start, end, str); + return this; + } + + // “在StringBuilder的位置index处插入‘str中从offset开始的内容’,插入内容长度是len” + public StringBuilder insert(int index, char[] str, int offset, + int len) + { + super.insert(index, str, offset, len); + return this; + } + + // “在StringBuilder的位置offset处插入obj对应的字符串” + public StringBuilder insert(int offset, Object obj) { + return insert(offset, String.valueOf(obj)); + } + + // “在StringBuilder的位置offset处插入str” + public StringBuilder insert(int offset, String str) { + super.insert(offset, str); + return this; + } + + // “在StringBuilder的位置offset处插入str” + public StringBuilder insert(int offset, char[] str) { + super.insert(offset, str); + return this; + } + + // “在StringBuilder的位置dstOffset处插入s” + public StringBuilder insert(int dstOffset, CharSequence s) { + if (s == null) + s = "null"; + if (s instanceof String) + return this.insert(dstOffset, (String)s); + return this.insert(dstOffset, s, 0, s.length()); + } + + // “在StringBuilder的位置dstOffset处插入's中从start到end的内容'” + public StringBuilder insert(int dstOffset, CharSequence s, + int start, int end) + { + super.insert(dstOffset, s, start, end); + return this; + } + + // “在StringBuilder的位置Offset处插入b” + public StringBuilder insert(int offset, boolean b) { + super.insert(offset, b); + return this; + } + + // “在StringBuilder的位置Offset处插入c” + public StringBuilder insert(int offset, char c) { + super.insert(offset, c); + return this; + } + + // “在StringBuilder的位置Offset处插入i” + public StringBuilder insert(int offset, int i) { + return insert(offset, String.valueOf(i)); + } + + // “在StringBuilder的位置Offset处插入l” + public StringBuilder insert(int offset, long l) { + return insert(offset, String.valueOf(l)); + } + + // “在StringBuilder的位置Offset处插入f” + public StringBuilder insert(int offset, float f) { + return insert(offset, String.valueOf(f)); + } + + // “在StringBuilder的位置Offset处插入d” + public StringBuilder insert(int offset, double d) { + return insert(offset, String.valueOf(d)); + } + + // 返回“str”在StringBuilder的位置 + public int indexOf(String str) { + return indexOf(str, 0); + } + + // 从fromIndex开始查找,返回“str”在StringBuilder的位置 + public int indexOf(String str, int fromIndex) { + return String.indexOf(value, 0, count, + str.toCharArray(), 0, str.length(),fromIndex); + } + + // 从后向前查找,返回“str”在StringBuilder的位置 + public int lastIndexOf(String str) { + return lastIndexOf(str, count); + } + + // 从fromIndex开始,从后向前查找,返回“str”在StringBuilder的位置 + public int lastIndexOf(String str, int fromIndex) { + return String.lastIndexOf(value, 0, count, + str.toCharArray(), 0, str.length(),fromIndex); + } + + // 反转StringBuilder + public StringBuilder reverse() { + super.reverse(); + return this; + } + + public String toString() { + // Create a copy, don't share the array + return new String(value, 0, count); + } + + // 序列化对应的写入函数 + private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + s.defaultWriteObject(); + s.writeInt(count); + s.writeObject(value); + } + + // 序列化对应的读取函数 + private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + s.defaultReadObject(); + count = s.readInt(); + value = (char[]) s.readObject(); + } +``` + +### 2、常用方法 + +#### insert + +```java +public static void main(String[] args) { + StringBuilder sbuilder = new StringBuilder(); + // 在位置0处插入字符数组 + sbuilder.insert(0, new char[]{'a', 'b', 'c', 'd', 'e'}); + // 在位置0处插入字符数组。0表示字符数组起始位置,3表示长度 + sbuilder.insert(0, new char[]{'A', 'B', 'C', 'D', 'E'}, 0, 3); + // 在位置0处插入float + sbuilder.insert(0, 1.414f); + // 在位置0处插入double + sbuilder.insert(0, 3.14159d); + // 在位置0处插入boolean + sbuilder.insert(0, true); + // 在位置0处插入char + sbuilder.insert(0, '\n'); + // 在位置0处插入int + sbuilder.insert(0, 100); + // 在位置0处插入long + sbuilder.insert(0, 12345L); + // 在位置0处插入StringBuilder对象 + sbuilder.insert(0, new StringBuilder("StringBuilder")); + // 在位置0处插入StringBuilder对象。6表示被在位置0处插入对象的起始位置(包括),13是结束位置(不包括) + sbuilder.insert(0, new StringBuilder("STRINGBUILDER"), 6, 13); + // 在位置0处插入StringBuffer对象。 + sbuilder.insert(0, new StringBuffer("StringBuffer")); + // 在位置0处插入StringBuffer对象。6表示被在位置0处插入对象的起始位置(包括),12是结束位置(不包括) + sbuilder.insert(0, new StringBuffer("STRINGBUFFER"), 6, 12); + // 在位置0处插入String对象。 + sbuilder.insert(0, "String"); + // 在位置0处插入String对象。1表示被在位置0处插入对象的起始位置(包括),6是结束位置(不包括) + sbuilder.insert(0, "0123456789", 1, 6); + sbuilder.insert(0, '\n'); + + // 在位置0处插入Object对象。此处以HashMap为例 + HashMap map = new HashMap(); + map.put("1", "one"); + map.put("2", "two"); + map.put("3", "three"); + + sbuilder.insert(0, map); + + System.out.printf("%s\n\n", sbuilder); +} +``` + +#### append + +```java +public static void main(String[] args) { + StringBuilder sbuilder = new StringBuilder(); + // 追加字符数组 + sbuilder.append(new char[]{'a', 'b', 'c', 'd', 'e'}); + // 追加字符数组。0表示字符数组起始位置,3表示长度 + sbuilder.append(new char[]{'A', 'B', 'C', 'D', 'E'}, 0, 3); + // 追加float + sbuilder.append(1.414f); + // 追加double + sbuilder.append(3.14159d); + // 追加boolean + sbuilder.append(true); + // 追加char + sbuilder.append('\n'); + // 追加int + sbuilder.append(100); + // 追加long + sbuilder.append(12345L); + // 追加StringBuilder对象 + sbuilder.append(new StringBuilder("StringBuilder")); + // 追加StringBuilder对象。6表示被追加对象的起始位置(包括),13是结束位置(不包括) + sbuilder.append(new StringBuilder("STRINGBUILDER"), 6, 13); + // 追加StringBuffer对象。 + sbuilder.append(new StringBuffer("StringBuffer")); + // 追加StringBuffer对象。6表示被追加对象的起始位置(包括),12是结束位置(不包括) + sbuilder.append(new StringBuffer("STRINGBUFFER"), 6, 12); + // 追加String对象。 + sbuilder.append("String"); + // 追加String对象。1表示被追加对象的起始位置(包括),6是结束位置(不包括) + sbuilder.append("0123456789", 1, 6); + + sbuilder.append('\n'); + + // 追加Object对象。此处以HashMap为例 + HashMap map = new HashMap(); + map.put("1", "one"); + map.put("2", "two"); + map.put("3", "three"); + sbuilder.append(map); + sbuilder.append('\n'); + + // 追加unicode编码 + sbuilder.appendCodePoint(0x5b57); // 0x5b57是“字”的unicode编码 + sbuilder.appendCodePoint(0x7b26); // 0x7b26是“符”的unicode编码 + sbuilder.appendCodePoint(0x7f16); // 0x7f16是“编”的unicode编码 + sbuilder.appendCodePoint(0x7801); // 0x7801是“码”的unicode编码 + + System.out.printf("%s\n\n", sbuilder); +} +``` + +#### replace + +```java +public static void main(String[] args) { + StringBuilder sbuilder; + + sbuilder = new StringBuilder("0123456789"); + sbuilder.replace(0, 3, "ABCDE"); + System.out.printf("sbuilder=%s\n", sbuilder); + + sbuilder = new StringBuilder("0123456789"); + sbuilder.reverse(); + System.out.printf("sbuilder=%s\n", sbuilder); + + sbuilder = new StringBuilder("0123456789"); + sbuilder.setCharAt(0, 'M'); + System.out.printf("sbuilder=%s\n", sbuilder); + System.out.println(); + /* + sbuilder=ABCDE3456789 + sbuilder=9876543210 + sbuilder=M123456789 +*/ +} + +``` + +#### delete + +```java +public static void main(String[] args) { + StringBuilder sbuilder = new StringBuilder("0123456789"); + + // 删除位置0的字符,剩余字符是“123456789”。 + sbuilder.deleteCharAt(0); + // 删除位置3(包括)到位置6(不包括)之间的字符,剩余字符是“123789”。 + sbuilder.delete(3, 6); + + // 获取sb中从位置1开始的字符串 + String str1 = sbuilder.substring(1); + // 获取sb中从位置3(包括)到位置5(不包括)之间的字符串 + String str2 = sbuilder.substring(3, 5); + // 获取sb中从位置3(包括)到位置5(不包括)之间的字符串,获取的对象是CharSequence对象,此处转型为String + String str3 = (String) sbuilder.subSequence(3, 5); + + System.out.printf("sbuilder=%s\nstr1=%s\nstr2=%s\nstr3=%s\n", + sbuilder, str1, str2, str3); + +} +``` + +#### index + +```java +public static void main(String[] args) { + StringBuilder sbuilder = new StringBuilder("abcAbcABCabCaBcAbCaBCabc"); + + System.out.printf("sbuilder=%s\n", sbuilder); + + // 1. 从前往后,找出"bc"第一次出现的位置 + System.out.printf("%-30s = %d\n", "sbuilder.indexOf(\"bc\")", sbuilder.indexOf("bc")); + + // 2. 从位置5开始,从前往后,找出"bc"第一次出现的位置 + System.out.printf("%-30s = %d\n", "sbuilder.indexOf(\"bc\", 5)", sbuilder.indexOf("bc", 5)); + + // 3. 从后往前,找出"bc"第一次出现的位置 + System.out.printf("%-30s = %d\n", "sbuilder.lastIndexOf(\"bc\")", sbuilder.lastIndexOf("bc")); + + // 4. 从位置4开始,从后往前,找出"bc"第一次出现的位置 + System.out.printf("%-30s = %d\n", "sbuilder.lastIndexOf(\"bc\", 4)", sbuilder.lastIndexOf("bc", 4)); +} +``` + +#### 其他API + +```java +public static void main(String[] args) { + StringBuilder sbuilder = new StringBuilder("0123456789"); + + int cap = sbuilder.capacity(); + System.out.printf("cap=%d\n", cap); + + /* + capacity()返回的是字符串缓冲区的容量 + StringBuffer( ); 分配16个字符的缓冲区 + StringBuffer( int len ); 分配len个字符的缓冲区 + StringBuffer( String s ); 除了按照s的大小分配空间外,再分配16个 字符的缓冲区 + 你的StringBuffer是用字符构造的,"0123456789"的长度是10另外再分配16个字符,所以一共是26。 + */ + + char c = sbuilder.charAt(6); + System.out.printf("c=%c\n", c); + + char[] carr = new char[4]; + sbuilder.getChars(3, 7, carr, 0); + for (int i = 0; i < carr.length; i++) { + System.out.printf("carr[%d]=%c ", i, carr[i]); + } + System.out.println(); + +} +``` + +### 3、StringBuffer + +和StringBulider用法差不多,不过多介绍,主要看一下三者的区别 + +StringBuffer是线程安全的,推荐在多线程里使用。 + +### 4、小结 + +String、StringBuffer、StringBuilder之间的区别 + +首先需要说明的是: + +- String 字符串常量 +- StringBuffer 字符串变量(线程安全) +- StringBuilder 字符串变量(非线程安全) + +在大多数情况下三者在执行速度方面的比较:StringBuilder > StringBuffer > String + +解释: + +String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象,因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。 + +而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。 + +为什么是大多数情况呢? + +在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接, 所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的: + +```java +String S1 = “This is only a” + “ simple” + “ test”; +StringBuffer Sb = new StringBuilder(“This is only a”).append(“simple”).append(“ test”); +``` + +你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本 一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个 + +String S1 = “This is only a” + “ simple” + “test”; + +其实就是:String S1 = “This is only a simple test”; + +所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象 的话,速度就没那么快了,譬如: + +> String S2 = “This is only a”; +> String S3 = “ simple”; +> String S4 = “ test”; + +大部分情况下StringBuilder的速度要大于StringBuffer: + +java.lang.StringBuilder一个可变的字符序列是5.0新增的。(大多数情况下就是我们是在单线程下进行的操作,所以大多数情况下是建议用StringBuilder而不用StringBuffer的)此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个 线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。 + +对于三者使用的总结: + +- 如果要操作少量的数据用 :String +- 单线程操作字符串缓冲区 下操作大量数据 :StringBuilder +- 多线程操作字符串缓冲区 下操作大量数据 : StringBuffer + +### 5、面试题的回答 + +StringBuilder 与StringBuffer的区别,StringBuilder与String的区别。 + +1)StringBuilder效率高,线程不安全,StringBuffer对比之下没它高,线程安全。 + +2)String是不可变字符串,StringBuilder是可变字符串。为什么有这样的差异,可以深入源码去解析, 比如String类内的 priver final char value[] 等方法的原因。 + +3)如果是简单的声明一个字符串没有后续过多的操作,使用String,StringBuilder均可,若后续对字符穿做频繁的添加,删除操作,或者是在循环当中动态的改变字符穿的长度应该用StringBuilder。使用String 会产生多余的字符串,占用内存空间。 + +## File类 + +### 1、File类的基本用法 + + java.io.File类:文件和目录路径名的抽象表示形式。 + +File类的常见构造方法: + +```java +public File(String pathname) +``` + +以pathname为路径创建File对象,如果pathname是相对路径,则默认的当前路径在系统属性user.dir 中存储。 + +File的静态属性String separator存储了当前系统的路径分隔符。 + +通过File对象可以访问文件的属性。 + +```java +public boolean canRead() +public boolean exists() +public boolean isFile() +public long lastModified() +public String getName() +public boolean canWrite() +public boolean isDirectory() +public boolean isHidden() +public long length() +public String getPath() +``` + +通过File对象创建空文件或目录(在该对象所指的文件或目录不存在的情况下)。 + +```java +public boolean createNewFile()throws IOException +public boolean delete() +public boolean mkdir(), mkdirs() +``` + +常见构造器,方法 + +```java +import java.io.File; +import java.io.IOException; +public class TestFile { + /** + * File文件类 1.代表文件 2.代表目录 + */ + public static void main(String[] args) { + File f = new File("d:/src3/TestObject.java"); + File f2 = new File("d:/src3"); + File f3 = new File(f2, "TestFile.java"); + File f4 = new File(f2, "TestFile666.java"); + File f5 = new File("d:/src3/aa/bb/cc/dd"); + //f5.mkdirs(); + f5.delete(); + + try { + f4.createNewFile(); + System.out.println("文件创建成功!"); + } catch (IOException e) { + e.printStackTrace(); + } + if (f.isFile()) { + System.out.println("是一个文件!"); + } + if (f2.isDirectory()) { + System.out.println("是一个目录!"); + } + if (f3.isFile()) { + System.out.println("是一个文件奥"); + } + } +} +``` + diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/01.\351\233\206\345\220\210\346\246\202\345\272\217.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/01.\351\233\206\345\220\210\346\246\202\345\272\217.md" new file mode 100644 index 00000000..bd496c76 --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/01.\351\233\206\345\220\210\346\246\202\345\272\217.md" @@ -0,0 +1,59 @@ +--- +title: 集合概序 +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/synopsis +categories: + - java + - java-se + - 集合框架 +--- + +## 集合概序 + +### 1、为什么使用集合框架? + +假设,一个班级有30个人,我们需要存储学员的信息,是不是我们可以用一个一维数组就解决了? + +那换一个问题,一个网站每天要存储的新闻信息,我们知道新闻是可以实时发布的,我们并不知道需要多大的空间去存储,我要是去设置一个很大的数组,要是没有存满,或者不够用,都会影响我们,前者浪费的空间,后者影响了业务! + +如果并不知道程序运行时会需要多少对象,或者需要更复杂的方式存储对象,那我们就可以使用Java的集合框架! + +### 2、集合框架包含的内容 + + Java集合框架提供了一套性能优良,使用方便的接口和类,他们位于java.util包中。 + +接口和具体类 + +![image-20210330134353432](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330134353432.png) + +算法 + +Collections 类提供了对集合进行排序,遍历等多种算法实现! + +【重中之重】 + +- Collection 接口存储一组不唯一,无序的对象 + +- List 接口存储一组不唯一,有序的对象。 + +- Set 接口存储一组唯一,无序的对象 + +- Map 接口存储一组键值对象,提供key到value的映射 + +- ArrayList实现了长度可变的数组,在内存中分配连续的空间。遍历元素和随机访问元素的效率比较高 + + ![image-20210330134530971](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330134530971.png) + + ![image-20210330134636140](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330134636140.png) + +- LinkedList采用链表存储方式。插入、删除元素时效率比较高 + + ![image-20210330134556335](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330134556335.png) + + ![image-20210330134705021](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330134705021.png) + +- HashSet:采用哈希算法实现的Set + + HashSet的底层是用HashMap实现的,因此查询效率较高,由于采用hashCode算法直接确定 元素的内存地址,增删效率也挺高的。 + + \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/02.ArrayList.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/02.ArrayList.md" new file mode 100644 index 00000000..15a4de0b --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/02.ArrayList.md" @@ -0,0 +1,613 @@ +--- +title: ArrayList +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/ArrayList +categories: + - java + - java-se + - 集合框架 +--- + +# ArrayList + +## ArrayList概述 + +ArrayList是可以动态增长和缩减的索引序列,它是基于数组实现的List类。 + +该类封装了一个动态再分配的Object[]数组,每一个类对象都有一个capacity【容量】属性,表示它们所封装的Object[]数组的长度,当向ArrayList中添加元素时,该属性值会自动增加。如果想 ArrayList中添加大量元素,可使用ensureCapacity方法一次性增加capacity,可以减少增加重分配的次数提高性能。 + +ArrayList的用法和Vector向类似,但是Vector是一个较老的集合,具有很多缺点,不建议使用。 + +另外,ArrayList和Vector的区别是:ArrayList是线程不安全的,当多条线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的。 + +ArrayList和Collection的关系: + +![image-20210330144341316](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330144341316.png) + +## ArrayList的数据结构 + +分析一个类的时候,数据结构往往是它的灵魂所在,理解底层的数据结构其实就理解了该类的实现思 路,具体的实现细节再具体分析。 + +ArrayList的数据结构是: + +![image-20210330144539830](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330144539830.png) + +说明:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。我们对 ArrayList类的实例的所有的操作底层都是基于数组的。 + +## ArrayList源码分析 + +### 1、继承结构和层次关系 + +IDEA快捷键:Ctrl+H + +![image-20210330144845625](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330144845625.png) + +```java +public class ArrayList extends AbstractList +implements List, RandomAccess, Cloneable, java.io.Serializable{ + +} +``` + +我们看一下ArrayList的继承结构: + +ArrayList extends AbstractList AbstractList extends AbstractCollection + +所有类都继承Object 所以ArrayList的继承结构就是上图这样。 + +【分析】 + +**为什么要先继承AbstractList,而让AbstractList先实现List?而不是让ArrayList直接实现List?** + +这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到 一个类上面还有一个抽象类,应该就是这个作用。 + +**ArrayList实现了哪些接口?** + +**List接口**:我们会出现这样一个疑问,在查看了ArrayList的父类 AbstractList也实现了List接口,那为什么子类ArrayList还是去实现一遍呢? + +这是想不通的地方,查资料显示,有的人说是为了查看代码方便,使观看者一目了然,说法不 一,但每一个让我感觉合理的,但是在stackOverFlow中找到了答案,这里其实很有趣。 + +开发这个collection 的作者Josh说: + +这其实是一个mistake[失误],因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。 + +**RandomAccess接口**:这个是一个标记性接口,通过查看api文档,它的作用就是用来快速随机存取, 有关效率的问题,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如ArrayList。而没有实现该接口的话,使用Iterator来迭代,这样性能更高,例如linkedList。所以这个标记性只是为了 让我们知道我们用什么样的方式去获取数据性能更好。 + +**Cloneable接口**:实现了该接口,就可以使用Object.Clone()方法了。 + +**Serializable接口**:实现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够 从类变成字节流传输,然后还能从字节流变成原来的类。 + +### 2、类中的属性 + +```java +public class ArrayList extends AbstractList +implements List, RandomAccess, Cloneable, java.io.Serializable + { + // 版本号 + private static final long serialVersionUID = 8683452581122892189L; + // 缺省容量 + private static final int DEFAULT_CAPACITY = 10; + // 空对象数组 + private static final Object[] EMPTY_ELEMENTDATA = {}; + // 缺省空对象数组 + private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; + // 元素数组 + transient Object[] elementData; + // 实际元素大小,默认为0 + private int size; + // 最大数组容量 + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; +} +``` + +### 3、构造方法 + +![image-20210330145750383](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330145750383.png) + +无参构造方法 + +```java +/* + Constructs an empty list with an initial capacity of ten. + 这里就说明了默认会给10的大小,所以说一开始arrayList的容量是10. +*/ +//ArrayList中储存数据的其实就是一个数组,这个数组就是elementData. +public ArrayList() { + super(); //调用父类中的无参构造方法,父类中的是个空的构造方法 + this.elementData = EMPTY_ELEMENTDATA; + //EMPTY_ELEMENTDATA:是个空的Object[], 将elementData初始化,elementData也是个Object[]类型。空的Object[]会给默认大小10,等会会解释什么时候赋值的。 +} +``` + +有参构造方法1 + +```java +/* +Constructs an empty list with the specified initial capacity. +构造具有指定初始容量的空列表。 + +@param initialCapacity the initial capacity of the list +初始容量列表的初始容量 + +@throws IllegalArgumentException if the specified initial capacity is negative +如果指定的初始容量为负,则为IllegalArgumentException +*/ + +public ArrayList(int initialCapacity) { + if (initialCapacity > 0) { + //将自定义的容量大小当成初始化 initialCapacity 的大小 + this.elementData = new Object[initialCapacity]; + } else if (initialCapacity == 0) { + this.elementData = EMPTY_ELEMENTDATA; //等同于无参构造方法 + } else { + //判断如果自定义大小的容量小于0,则报下面这个非法数据异常 + throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); + } +} +``` + +有参构造方法 2 + +```java +/* + Constructs a list containing the elements of the specified collection,in the order they are returned by the collection's iterator. + 按照集合迭代器返回元素的顺序构造包含指定集合的元素的列表。 + @param c the collection whose elements are to be placed into this list + @throws NullPointerException if the specified collection is null +*/ +public ArrayList(Collection c) { + elementData = c.toArray(); //转换为数组 + //每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么久需要使用ArrayList中的方法去改造一下。 + if ((size = elementData.length) != 0) { + // c.toArray might (incorrectly) not return Object[] (see 6260652) + if (elementData.getClass() != Object[].class) + elementData = Arrays.copyOf(elementData, size, Object[].class); + } else { + // replace with empty array. + this.elementData = EMPTY_ELEMENTDATA; + } +} +``` + +这个构造方法不常用,举个例子就能明白什么意思 + +举个例子: Strudent exends Person , ArrayList、 Person这里就是泛型 , 我还有一个Collection、 由于这个Student继承了Person,那么根据这个构造方法,我就可以把这个Collection转换为ArrayList , 这就是这个构造方法的作用 。 + +【总结】ArrayList的构造方法就做一件事情,就是初始化一下储存数据的容器,其实本质上就是一个数 组,在其中就叫elementData。 + +### 4、核心方法-add + +**alt+7 查看方法列表,ctrl+左键 选中add进去后查看** + +**boolean add(E)** + +```java +/** +* Appends the specified element to the end of this list. +* 添加一个特定的元素到list的末尾。 +* @param e element to be appended to this list +* @return true (as specified by {@link Collection#add}) +*/ +public boolean add(E e) { + //确定内部容量是否够了,size是数组中数据的个数,因为要添加一个元素,所以size+1,先判断size+1的这个数数组能否放得下,就在这个方法中去判断是否数组.length是否够用了。 + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; //在数据中正确的位置上放上元素e,并且size++ + return true; +} +``` + +【分析:ensureCapacityInternal(xxx); 确定内部容量的方法】 + +```java +private void ensureCapacityInternal(int minCapacity) { + ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); +} + +private static int calculateCapacity(Object[] elementData, int minCapacity){ + //看,判断初始化的elementData是不是空的数组,也就是没有长度 + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + //因为如果是空的话,minCapacity=size+1;其实就是等于1,空的数组没有长度就存放不了,所以就将minCapacity变成10,也就是默认大小,但是在这里,还没有真正的初始化这个elementData的大小。 + return Math.max(DEFAULT_CAPACITY, minCapacity); + } + //确认实际的容量,上面只是将minCapacity=10,这个方法就是真正的判断elementData是否够用 + return minCapacity; +} + +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + // overflow-conscious code + //minCapacity如果大于了实际elementData的长度,那么就说明elementData数组的长度不够用,不够用那么就要增加elementData的length。这里有的同学就会模糊minCapacity到底是什么呢,这里给你们分析一下 + /* + 第一种情况:由于elementData初始化时是空的数组,那么第一次add的时候,minCapacity=size+1;也就minCapacity=1,在上一个方法(确定内部容量ensureCapacityInternal)就会判断出是空的数组,就会给将minCapacity=10,到这一步为止,还没有改变elementData的大小。 + + 第二种情况:elementData不是空的数组了,那么在add的时候,minCapacity=size+1;也就是minCapacity代表着elementData中增加之后的实际数据个数,拿着它判断elementData的length是否够用,如果length不够用,那么肯定要扩大容量,不然增加的这个元素就会溢出。 +*/ + if (minCapacity - elementData.length > 0) + grow(minCapacity); +} +//arrayList核心的方法,能扩展数组大小的真正秘密。 +private void grow(int minCapacity) { + // overflow-conscious code + + //将扩充前的elementData大小给oldCapacity + int oldCapacity = elementData.length; + + //newCapacity就是1.5倍的oldCapacity + int newCapacity = oldCapacity + (oldCapacity >> 1); + + //这句话就是适应于elementData就空数组的时候,length=0,那么oldCapacity=0,newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10.前面的工作都是准备工作。 + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + + //如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + //新的容量大小已经确定好了,就copy数组,改变容量大小咯。 + elementData = Arrays.copyOf(elementData, newCapacity); +} + + +//这个就是上面用到的方法,很简单,就是用来赋最大值 +private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) // overflow + throw new OutOfMemoryError(); + +//如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回。因为maxCapacity是三倍的minCapacity,可能扩充的太大了,就用minCapacity来判断了。 + +//Integer.MAX_VALUE:2147483647 MAX_ARRAY_SIZE:2147483639 也就是说最大也就能给到第一个数值。还是超过了这个限制,就要溢出了。相当于arraylist给了两层防护。 + return (minCapacity > MAX_ARRAY_SIZE) ? + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; +} + + +``` + + + +**void add(int,E)** + +```java +public void add(int index, E element) { + //检查index也就是插入的位置是否合理。 + rangeCheckForAdd(index); + + ensureCapacityInternal(size + 1); // Increments modCount!! + + //这个方法就是用来在插入元素之后,要将index之后的元素都往后移一位, + System.arraycopy(elementData, index, elementData, index + 1, + size - index); + + //在目标位置上存放元素 + elementData[index] = element; + size++; +} +``` + +【分析:rangeCheckForAdd(index)】 + +```java +private void rangeCheckForAdd(int index) { + //插入的位置肯定不能大于size 和小于0 + if (index > size || index < 0) + //如果是,就报这个越界异常 + throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); +} +``` + +【System.arraycopy(...):就是将elementData在插入位置后的所有元素,往后面移一位】 + +```java +public static void arraycopy(Object src, + int srcPos, + Object dest, + int destPos, + int length) + + //src:源对象 + //srcPos:源对象对象的起始位置 + //dest:目标对象 + //destPost:目标对象的起始位置 + //length:从起始位置往后复制的长度。 + +``` + +**注释解读:** + +这段的大概意思就是解释这个方法的用法,复制src到dest,复制的位置是从src的srcPost开始,到srcPost+length-1的位置结束,复制到destPost上,从destPost开始到destPost+length-1的位置上 + +![image-20210330155111328](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330155111328.png) + +告诉你复制的一种情况,如果A和B是一样的,那么先将A复制到临时数组C,然后通过C复制到B,用了一个第三方参数 + +![image-20210330155151694](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330155151694.png) + +这一大段,就是来说明会出现的一些问题,NullPointerException和IndexOutOfBoundsException 还有ArrayStoreException 这三个异常出现的原因。 + +![image-20210330155313786](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330155313786.png) + +这里描述了一种特殊的情况,就是当A的长度大于B的长度的时候,会复制一部分,而不是完全失败。 + +![image-20210330155350320](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330155350320.png) + +这些是参数列表的解释和异常 + +![image-20210330155421252](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330155421252.png) + +:scream:读别人源码是件很头疼的事,新手会用就行,原理等以后回来复习的时候再看~ + +【总结】 + +正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。 + +当我们调用add方法时,实际上的函数调用如下: + +![image-20210330161507771](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330161507771.png) + +说明:程序调用add,实际上还会进行一系列调用,可能会调用到grow,grow可能会调用hugeCapacity。 + +【举例】 + +```java +List lists = new ArrayList; +lists.add(8); +``` + +说明:初始化lists大小为0,调用的ArrayList()型构造函数,那么在调用lists.add(8)方法时,会经过怎样 的步骤呢?下图给出了该程序执行过程和最初与最后的elementData的大小。 + +![image-20210330161955630](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330161955630.png) + +说明:我们可以看到,在add方法之前开始elementData = {};调用add方法时会继续调用,直至 grow,最后elementData的大小变为10,之后再返回到add函数,把8放在elementData[0]中。 + +【举例说明二】 + +```java +List lists = new ArrayList(6); +lists.add(8); +``` + +说明:调用的ArrayList(int)型构造函数,那么elementData被初始化为大小为6的Object数组,在调用add(8)方法时,具体的步骤如下: + +![image-20210330162415006](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330162415006.png) + +说明:我们可以知道,在调用add方法之前,elementData的大小已经为6,之后再进行传递,不会进行 扩容处理。 + +推荐文章系列: [【集合框架】JDK1.8源码分析之HashMap(一)](https://www.cnblogs.com/leesf456/p/5242233.html) + +### 5、核心方法-remove + +其实这几个删除方法都是类似的。我们选择几个讲,其中fastRemove(int)方法是private的,是提供给 remove(Object)这个方法用的。 + +remove(int):通过删除指定位置上的元素 + +```java +public E remove(int index) { + rangeCheck(index);//检查index的合理性 + + modCount++;//这个作用很多,比如用来检测快速失败的一种标志。 + E oldValue = elementData(index);//通过索引直接找到该元素 + + int numMoved = size - index - 1;//计算要移动的位数。 + if (numMoved > 0) + //这个方法也已经解释过了,就是用来移动元素的。 + System.arraycopy(elementData, index+1, elementData, index,numMoved); + //将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。 + elementData[--size] = null; // clear to let GC do its work + //返回删除的元素。 + return oldValue; +} +``` + +remove(Object):这个方法可以看出来,arrayList是可以存放null值 + +```java +//通过元素来删除该元素,就依次遍历,如果有这个元素,就将该元素的索引传给fastRemobe(index),使用这个方法来删除该元素, +//fastRemove(index)方法的内部跟remove(index)的实现几乎一样,这里最主要是知道arrayList可以存储null值 +public boolean remove(Object o) { + if (o == null) { + for (int index = 0; index < size; index++) + if (elementData[index] == null) { + fastRemove(index); + return true; + } + } else { + for (int index = 0; index < size; index++) + if (o.equals(elementData[index])) { + fastRemove(index); + return true; + } + } + return false; +} +``` + +clear():将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,所以叫clear + +```java +public void clear() { + modCount++; + + // clear to let GC do its work + for (int i = 0; i < size; i++) + elementData[i] = null; + size = 0; +} +``` + +removeAll(collection c) + +```java +public boolean removeAll(Collection c) { + return batchRemove(c, false);//批量删除 +} +``` + +batchRemove(xx,xx):用于两个方法,一个removeAll():它只清楚指定集合中的元素,retainAll() 用来测试两个集合是否有交集。 + +```java +//这个方法,用于两处地方,如果complement为false,则用于removeAll如果为true,则给retainAll()用,retainAll()是用来检测两个集合是否有交集的。 + +private boolean batchRemove(Collection c, boolean complement) { + final Object[] elementData = this.elementData; //将原集合,记名为A + int r = 0, w = 0; //r用来控制循环,w是记录有多少个交集 + boolean modified = false; + try { + for (; r < size; r++) + //参数中的集合C一次检测集合A中的元素是否有, + if (c.contains(elementData[r]) == complement) + //有的话,就给集合A + elementData[w++] = elementData[r]; + } finally { + // Preserve behavioral compatibility with AbstractCollection, + // even if c.contains() throws. + //如果contains方法使用过程报异常 + if (r != size) { + //将剩下的元素都赋值给集合A, + System.arraycopy(elementData, r, + elementData, w, + size - r); + w += size - r; + } + if (w != size) { + // clear to let GC do its work + //这里有两个用途,在removeAll()时,w一直为0,就直接跟clear一样,全是为null。 + //retainAll():没有一个交集返回true,有交集但不全交也返回true,而两个集合相等的时候,返回false,所以不能根据返回值来确认两个集合是否有交集,而是通过原集合的大小是否发生改变来判断,如果原集合中还有元素,则代表有交集,而元集合没有元素了,说明两个集合没有交集。 + + for (int i = w; i < size; i++) + elementData[i] = null; + modCount += size - w; + size = w; + modified = true; + } + } + return modified; + } +``` + +总结:remove函数,用户移除指定下标的元素,此时会把指定下标到数组末尾的元素向前移动一个单 位,并且会把数组最后一个元素设置为null,这样是为了方便之后将整个数组不被使用时,会被GC,可 以作为小的技巧使用。 + +### 6、其他方法 + +**set()方法** + +说明:设定指定下标索引的元素值 + +```java +public E set(int index, E element) { + // 检验索引是否合法 + rangeCheck(index); + // 旧值 + E oldValue = elementData(index); + // 赋新值 + elementData[index] = element; + // 返回旧值 + return oldValue; +} +``` + +**indexOf()方法** + +说明:从头开始查找与指定元素相等的元素,注意,是可以查找null元素的,意味着ArrayList中可以存放null元素的。与此函数对应的lastIndexOf,表示从尾部开始查找。 + +```java +// 从首开始查找数组里面是否存在指定元素 +public int indexOf(Object o) { + if (o == null) { // 查找的元素为空 + for (int i = 0; i < size; i++) // 遍历数组,找到第一个为空的元素,返回下标 + if (elementData[i]==null) + return i; + } else { // 查找的元素不为空 + for (int i = 0; i < size; i++) // 遍历数组,找到第一个和指定元素相等的元素,返回下标 + if (o.equals(elementData[i])) + return i; + } + // 没有找到,返回空 + return -1; +} +``` + +**get()方法** + +```java +public E get(int index) { + // 检验索引是否合法 + rangeCheck(index); + return elementData(index); +} +``` + +说明:get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0),值得注意的 是,在get函数中存在element函数,element函数用于返回具体的元素,具体函数如下: + +```java +E elementData(int index) { + return (E) elementData[index]; +} +``` + +说明:返回的值都经过了向下转型(Object -> E),这些是对我们应用程序屏蔽的小细节。 + +## ArrayList实践 + +问题:我们现在有4只小狗,我们如何存储它的信息,获取总数,并能够逐条打印狗狗信息! + +分析:通过List 接口的实现类ArrayList 实现该需求。 + +- 元素个数不确定 +- 要求获得元素的实际个数 +- 按照存储顺序获取并打印元素信息 + +```java +class Dog { + private String name; + //构造。。。set、get、。。。toString() +} +``` + +```java +public class TestArrayList { + public static void main(String[] args) { + //创建ArrayList对象 , 并存储狗狗 + List dogs = new ArrayList(); + + dogs.add(new Dog("小狗一号")); + dogs.add(new Dog("小狗二号")); + dogs.add(new Dog("小狗三号")); + dogs.add(2,new Dog("小狗四号"));// 添加到指定位置 + + // .size() : ArrayList大小 + System.out.println("共计有" + dogs.size() + "条狗狗。"); + System.out.println("分别是:"); + + // .get(i) : 逐个获取个元素 + for (int i = 0; i < dogs.size(); i++) { + Dog dog = (Dog) dogs.get(i); + System.out.println(dog.getName()); + } + } +} +``` + +问题联想: + +- 删除第一个狗狗 :remove(index) +- 删除指定位置的狗狗 :remove(object) +- 判断集合中是否包含指定狗狗 : contains(object) + +分析:使用List接口提供的remove()、contains()方法 + +【常用方法】 + +![image-20210330141203456](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330141203456.png) + + + +# 总结 + +1)arrayList可以存放null。 +2)arrayList本质上就是一个elementData数组。 +3)arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是gorw()方法。 +4)arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全是删除集合中的元素。 +5)arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很 +多,有移动很多数据才能达到应有的效果 +6)arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。 \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/03.LinkedList.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/03.LinkedList.md" new file mode 100644 index 00000000..4644a649 --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/03.LinkedList.md" @@ -0,0 +1,660 @@ +--- +title: LinkedList +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/LinkedList +categories: + - java + - java-se + - 集合框架 +--- + +# LinkedList + +## 引入 + +问题:在集合的任何位置(头部,中间,尾部)添加,获取,删除狗狗对象! + +插入,删除操作频繁时,可使用LinkedList来提高效率 + +LinkedList提供对头部和尾部元素进行添加和删除操作的方法! + +![image-20210330184629619](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330184629619.png) + +**LinkedList的特殊方法** + +![image-20210330184756855](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330184756855.png) + +集合框架有何好处? +Java集合框架中包含哪些接口和类? +ArrayList和LinkedList有何异同? + + + +## LinkedList概述 + +我们都知道它的底层是由链表实现的,所以我们要明白什么是链表? + +![img](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/050053434697439.png) + +LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现的。 + +LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。 + +LinkedList 实现 List 接口,能对它进行队列操作。 + +LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。 + +LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。 + +LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。 + +LinkedList 是非同步的。 + +推荐文章:[LinkList详解](https://blog.csdn.net/qedgbmwyz/article/details/80108618) + +## 链表的数据结构 + +**单向链表:** + + element:用来存放元素 + + next:用来指向下一个节点元素 + +通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。 + +![image-20210330215243376](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330215243376.png) + +**单向循环链表:** + + element、next 跟前面一样,在单向链表的最后一个节点的next会指向头节点,而不是指向null,这样存成一个环 + +![image-20210330215349904](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330215349904.png) + + + +**双向链表:** + +element:存放元素 + +pre:用来指向前一个元素 + +next:指向后一个元素 + +双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。 + +![image-20210330215814374](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330215814374.png) + + + +**双向循环链表:** + + element、pre、next 跟前面的一样 + +第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”。 + +![image-20210330215757946](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330215757946.png) + +**【LinkedList的数据结构】** + +![image-20210330215856514](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330215856514.png) + +如上图所示,LinkedList底层使用的双向链表结构,有一个头结点和一个尾结点,双向链表意味着我们可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。 + + + +## LinkedList源码分析 + +接下来又到了大家喜欢的读源码环节**:smile:** + +### 1、LinkedList的特性 + +在我们平常中,我们只知道一些常识性的特点: + +1)是通过链表实现的 +2)如果在频繁的插入,或者删除数据时,就用linkedList性能会更好。 + +那我们通过API去查看它的一些特性 + +>Doubly-linked list implementation of the List and Deque interfaces. Implements all optional list operations, and permits all elements (including null). +> +>这告诉我们,linkedList是一个双向链表,并且实现了List和Deque接口中所有的列表操作,并且能存储任何元素,包括null,这里我们可以知道linkedList除了可以当链表使用,还可以当作队列使用,并能进行相应的操作。 +> +>All of the operations perform as could be expected for a doubly-linked list. Operations that index into the list will traverse the list from the beginning or the end, whichever is closer to the specified index. +> +>这个告诉我们,linkedList在执行任何操作的时候,都必须先遍历此列表来靠近通过index查找我们所需要的的值。通俗点讲,这就告诉了我们这个是顺序存取,每次操作必须先按开始到结束的顺序遍历,随机存取,就是arrayList,能够通过index。随便访问其中的任意位置的数据,这就是随机列表的意思。 + +3)api中接下来讲的一大堆,就是说明linkedList是一个非线程安全的(异步),其中在操作Interator时, 如果改变列表结构(adddelete等),会发生fail-fast。 + +**通过API再次总结一下LinkedList的特性:** + +1. 异步,也就是非线程安全 + +2. 双向链表。由于实现了list和Deque接口,能够当作队列来使用。 + + 链表:查询效率不高,但是插入和删除这种操作性能好。 + +3. 是顺序存取结构(注意和随机存取结构两个概念搞清楚) + + + +### 2、继承结构以及层次关系 + +ctrl+h + +![image-20210330221055477](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330221055477.png) + +我们可以看到,linkedList在最底层,说明他的功能最为强大,并且细心的还会发现,arrayList有四层,这里多了一层AbstractSequentialList的抽象类,为什么呢? + +**通过API我们会发现:** + + 1)减少实现顺序存取(例如LinkedList)这种类的工作,通俗的讲就是方便,抽象出类似LinkedList这种类的一些共同的方法 + +2)既然有了上面这句话,那么以后如果自己想实现顺序存取这种特性的类(就是链表形式),那么就继承这个AbstractSequentialList抽象类,如果想像数组那样的随机存取的类,那么就去实现AbstracList抽象类。 + +3)这样的分层,就很符合我们抽象的概念,越在高处的类,就越抽象,往在底层的类,就越有自己独特的个性。自己要慢慢领会这种思想。 + +4)LinkedList的类继承结构很有意思,我们着重要看是Deque接口,Deque接口表示是一个双端队列,那么也意味着LinkedList是双端队列的一种实现,所以,基于双端队列的操作在LinkedList中全部有效。 + +```java +public abstract class AbstractSequentialList +extends AbstractList +``` + + + +![image-20210330221915604](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330221915604.png) + +第一段: + +> 这里第一段就解释了这个类的作用,这个类为实现list接口提供了一些重要的方法, +> 尽最大努力去减少实现这个“顺序存取”的特性的数据存储(例如链表)的什么 +> 随机存取数据(例如数组)的类应该优先使用AbstractList +> 从上面就可以大概知道,AbstractSwquentialList这个类是为了减少LinkedList这种顺序存取的类的代码复杂度而抽象的一个类, + +第二段: + +> 这一段大概讲的就是这个AbstractSequentialList这个类和AbstractList这个类是完全相反 的。比如get、add这个方法的实现 + +第三段: + +> 这里就是讲一些我们自己要继承该类,该做些什么事情,一些规范。 + + + +**【接口实现分析】** + +```java +public class LinkedList + extends AbstractSequentialList + implements List, Deque, Cloneable, java.io.Serializable + { + + } +``` + +1. List接口:列表,add、set、等一些对列表进行操作的方法 + +2. Deque接口:有队列的各种特性, + +3. Cloneable接口:能够复制,使用那个copy方法。 + +4. Serializable接口:能够序列化。 + +5. 应该注意到没有RandomAccess:那么就推荐使用iterator,在其中就有一个foreach,增强的for循环,其中原理也就是iterator,我们在使用的时候,使用foreach或者iterator都可以。 + + iterator: + + ```java + public static void method() { + List l = new ArrayList<>(); + + l.add("hello"); + l.add(0, "123"); + l.add("789"); + l.set(0, "456"); + l.remove(0); + + ListIterator lit = l.listIterator(); + while (lit.hasNext()) { + String next = lit.next(); + // l.add("111"); + lit.add("111"); + System.out.println(next); + + } + System.out.println(l); + } + ``` + +### 3、类的属性 + +```java +public class LinkedList + extends AbstractSequentialList + implements List, Deque, Cloneable, java.io.Serializable +{ + // 实际元素个数 + transient int size = 0; + // 头结点 + transient Node first; + // 尾结点 + transient Node last; +} +``` + +LinkedList的属性非常简单,一个头结点、一个尾结点、一个表示链表中实际元素个数的变量。注意, 头结点、尾结点都有transient关键字修饰,这也意味着在序列化时该域是不会序列化的。 + +### 4、构造方法 + +两个构造方法(两个构造方法都是规范规定需要写的) + +【空参构造函数】 + +```java +public LinkedList() { + +} +``` + +【有参构造函数】 + +```java +//将集合c中的各个元素构建成LinkedList链表。 +public LinkedList(Collection c) { + // 调用无参构造函数 + this(); + // 添加集合中所有的元素 + addAll(c); +} +``` + +说明:会调用无参构造函数,并且会把集合中所有的元素添加到LinkedList中。 + +### 5、内部类(Node) + +```java +//根据前面介绍双向链表就知道这个代表什么了,linkedList的奥秘就在这里。 +private static class Node { + E item; // 数据域(当前节点的值) + Node next; // 后继(指向当前一个节点的后一个节点) + Node prev; // 前驱(指向当前节点的前一个节点) + + // 构造函数,赋值前驱后继 + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } +} +``` + +说明:内部类Node就是实际的结点,用于存放实际元素的地方。 + +### 6、核心方法 + +#### add()方法 + +```java +public boolean add(E e) { + // 添加到末尾 + linkLast(e); + return true; +} +``` + +说明:add函数用于向LinkedList中添加一个元素,并且添加到链表尾部。具体添加到尾部的逻辑是由 linkLast函数完成的。 + + + +**【LinkLast(XXXXX)】** + +```java +/** + * Links e as last element. + */ +void linkLast(E e) { + final Node l = last; //临时节点l(L的小写)保存last,也就是l指向了最后一个节点 + final Node newNode = new Node<>(l, e, null);////将e封装为节点,并且e.prev指向了最后一个节点 + last = newNode;//newNode成为了最后一个节点,所以last指向了它 + if (l == null)//判断是不是一开始链表中就什么都没有,如果没有,则newNode就成为了第一个节点,first和last都要指向它 + first = newNode; + else //正常的在最后一个节点后追加,那么原先的最后一个节点的next就要指向现在真正的最后一个节点,原先的最后一个节点就变成了倒数第二个节点 + l.next = newNode; + size++; ;//添加一个节点,size自增 + modCount++; +} +``` + +说明:对于添加一个元素至链表中会调用add方法 -> linkLast方法。 + +【举例一】 + +```java +List lists = new LinkedList(); +lists.add(5); +lists.add(6); +``` + +首先调用无参构造函数,之后添加元素5,之后再添加元素6。具体的示意图如下: + +![image-20210330223637307](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330223637307.png) + +上图的表明了在执行每一条语句后,链表对应的状态。 + +#### addAll()方法 + +addAll有两个重载函数,addAll(Collection)型和addAll(int, Collection) 型,我们平时习惯调用的addAll(Collection)型会转化为addAll(int, Collection)型。 + +```java +public boolean addAll(Collection c) { +//继续往下看 +return addAll(size, c); +} +``` + +addAll(size,c):这个方法,能包含三种情况下的添加,我们这里分析的只是构造方法,空链表的情况,看的时候只需要按照不同的情况分析下去就行了。 + +```java +//真正核心的地方就是这里了,记得我们传过来的是size,c +public boolean addAll(int index, Collection c) { + //检查index这个是否为合理。这个很简单,自己点进去看下就明白了。 + checkPositionIndex(index); + //将集合c转换为Object数组 a + Object[] a = c.toArray(); + //数组a的长度numNew,也就是由多少个元素 + int numNew = a.length; + if (numNew == 0) + //集合c是个空的,直接返回false,什么也不做。 + return false; + //集合c是非空的,定义两个节点(内部类),每个节点都有三个属性,item、next、prev。注意:不要管这两个什么含义,就是用来做临时存储节点的。这个Node看下面一步的源码分析,Node就是linkedList的最核心的实现,可以直接先跳下一个去看Node的分析 + Node pred, succ; + //构造方法中传过来的就是index==size + if (index == size) { + //linkedList中三个属性:size、first、last。 size:链表中的元素个数。first:头节点 last:尾节点,就两种情况能进来这里 + //情况一、:构造方法创建的一个空的链表,那么size=0,last、和first都为null。linkedList中是空的。什么节点都没有。succ=null、pred=last=null + + //情况二、:链表中有节点,size就不是为0,first和last都分别指向第一个节点,和最后一个节点,在最后一个节点之后追加元素,就得记录一下最后一个节点是什么,所以把last保存到pred临时节点中。 + + succ = null; + pred = last; + } else { + //情况三、index!=size,说明不是前面两种情况,而是在链表中间插入元素,那么就得知道index上的节点是谁,保存到succ临时节点中,然后将succ的前一个节点保存到pred中,这样保存了这两个节点,就能够准确的插入节点了 + //举个简单的例子,有2个位置,1、2、如果想插数据到第二个位置,双向链表中,就需要知道第一个位置是谁,原位置也就是第二个位置上是谁,然后才能将自己插到第二个位置上。如果这里还不明白,先看一下文章开头对于各种链表的删除,add操作是怎么实现的。 + succ = node(index); + pred = succ.prev; + } + //前面的准备工作做完了,将遍历数组a中的元素,封装为一个个节点。 + for (Object o : a) { + @SuppressWarnings("unchecked") E e = (E) o; + //pred就是之前所构建好的,可能为null、也可能不为null,为null的话就是属于情况一、不为null则可能是情况二、或者情况三 + + Node newNode = new Node<>(pred, e, null); + //如果pred==null,说明是情况一,构造方法,是刚创建的一个空链表,此时的newNode就当作第一个节点,所以把newNode给first头节点 + if (pred == null) + first = newNode; + else + //如果pred!=null,说明可能是情况2或者情况3,如果是情况2,pred就是last,那么在最后一个节点之后追加到newNode,如果是情况3,在中间插入,pred为原index节点之前的一个节点,将它的next指向插入的节点,也是对的 + pred.next = newNode; + //然后将pred换成newNode,注意,这个不在else之中,请看清楚了。 + pred = newNode; + } + + if (succ == null) { + /*如果succ==null,说明是情况一或者情况二, + 情况一、构造方法,也就是刚创建的一个空链表,pred已经是newNode了,last=newNode,所以linkedList的first、last都指向第一个节点。 + 情况二、在最后节后之后追加节点,那么原先的last就应该指向现在的最后一个节点 +了,就是newNode。*/ + last = pred; + } else { + //如果succ!=null,说明可能是情况三、在中间插入节点,举例说明这几个参数的意义,有1、2两个节点,现在想在第二个位置插入节点newNode,根据前面的代码,pred=newNode,succ=2,并且1.next=newNode,已经构建好了,pred.next=succ,相当于在newNode.next =2; succ.prev = pred,相当于 2.prev = newNode, 这样一来,这种指向关系就完成了。first和last不用变,因为头节点和尾节点没变 + pred.next = succ; + succ.prev = pred; + } + //增加了几个元素,就把 size = size +numNew 就可以了 + size += numNew; + modCount++; + return true; +} +``` + +说明:参数中的index表示在索引下标为index的结点(实际上是第index + 1个结点)的前面插入。 + +在addAll函数中,addAll函数中还会调用到node函数,get函数也会调用到node函数,此函数是根据索引下标找到该结点并返回,具体代码如下: + +```java +Node node(int index) { + // assert isElementIndex(index); + // 判断插入的位置在链表前半段或者是后半段 + if (index < (size >> 1)) {// 插入位置在前半段 + Node x = first; + for (int i = 0; i < index; i++)// 从头结点开始正向遍历 + x = x.next; + return x;// 返回该结点 + } else {// 插入位置在后半段 + Node x = last; + for (int i = size - 1; i > index; i--)// 从尾结点开始反向遍历 + x = x.prev; + return x;// 返回该结点 + } +} +``` + +说明:在根据索引查找结点时,会有一个小优化,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样就保证了只需要遍历最多一半结点就可以找到指定索引的结点。 + +举例说明调用addAll函数后的链表状态: + +```java +List lists = new LinkedList(); +lists.add(5); +lists.addAll(0, Arrays.asList(2, 3, 4, 5)); +``` + +上述代码内部的链表结构如下: + +![image-20210330225517390](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330225517390.png) + +**addAll()中的一个问题:** + +在addAll函数中,传入一个集合参数和插入位置,然后将集合转化为数组,然后再遍历数组,挨个添加数组的元素,但是问题来了,为什么要先转化为数组再进行遍历,而不是直接遍历集合呢? + +从效果上两者是完全等价的,都可以达到遍历的效果。关于为什么要转化为数组的问题,我的思考如 下: + +1. 如果直接遍历集合的话,那么在遍历过程中需要插入元素,在堆上分配内存空间,修改指针域,这个过程中就会一直占用着这个集合,考虑正确同步的话,其他线程只能一直等待。 +2. 如果转化为数组,只需要遍历集合,而遍历集合过程中不需要额外的操作,所以占用的时间相对是较短的,这样就利于其他线程尽快的使用这个集合。说白了,就是有利于提高多线程访问该集合的效率,尽可能短时间的阻塞。 + +#### remove(Object o) + +```java +/** + * Removes the first occurrence of the specified element from this list, + * if it is present. If this list does not contain the element, it is + * unchanged. More formally, removes the element with the lowest index + * {@code i} such that + * (o==null ? get(i)==null : o.equals(get(i))) + * (if such an element exists). Returns {@code true} if this list + * contained the specified element (or equivalently, if this list + * changed as a result of the call). + * + * @param o element to be removed from this list, if present + * @return {@code true} if this list contained the specified element + */ + +///首先通过看上面的注释,我们可以知道,如果我们要移除的值在链表中存在多个一样的值,那么我们会移除index最小的那个,也就是最先找到的那个值,如果不存在这个值,那么什么也不做。 +public boolean remove(Object o) { + //这里可以看到,linkedList也能存储null + if (o == null) { + //循环遍历链表,直到找到null值,然后使用unlink移除该值。下面的这个else中也一样 + for (Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} +``` + +【unlink(xxxx)】 + +```java +// Unlinks non-null node x. +//不能传一个null值过,注意,看之前要注意之前的next、prev这些都是谁。 +E unlink(Node x) { + // assert x != null; + //拿到节点x的三个属性 + final E element = x.item; + final Node next = x.next; + final Node prev = x.prev; + + //这里开始往下就进行移除该元素之后的操作,也就是把指向哪个节点搞定。 + if (prev == null) { + //说明移除的节点是头节点,则first头节点应该指向下一个节点 + first = next; + } else { + //不是头节点,prev.next=next:有1、2、3,将1.next指向3 + prev.next = next; + //然后解除x节点的前指向。 + x.prev = null; + } + + if (next == null) { + //说明移除的节点是尾节点 + last = prev; + } else { + //不是尾节点,有1、2、3,将3.prev指向1. 然后将2.next=解除指向。 + next.prev = prev; + x.next = null; + } + + //x的前后指向都为null了,也把item为null,让gc回收它 + x.item = null; + size--;//移除一个节点,size自减 + modCount++; + return element;//由于一开始已经保存了x的值到element,所以返回。 +} +``` + +#### get(index) + +【get(index)查询元素的方法】 + +```java +/** + * Returns the element at the specified position in this list. + * + * @param index index of the element to return + * @return the element at the specified position in this list + * @throws IndexOutOfBoundsException {@inheritDoc} +*/ +//这里没有什么,重点还是在node(index)中 +public E get(int index) { + checkElementIndex(index); + return node(index).item; +} +``` + +【node(index)】 + +```java +/** +* Returns the (non-null) Node at the specified element index. +*/ +//这里查询使用的是先从中间分一半查找 +Node node(int index) { + // assert isElementIndex(index); + //"<<":*2的几次方 “>>”:/2的几次方,例如:size<<1:size*2的1次方, + //这个if中就是查询前半部分 + if (index < (size >> 1)) {//index x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else {//前半部分没找到,所以找后半部分 + Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +``` + +#### indexOf(Object o) + +```java +//这个很简单,就是通过实体元素来查找到该元素在链表中的位置。跟remove中的代码类似,只是返回类型不一样。 +public int indexOf(Object o) { + int index = 0; + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) + return index; + index++; + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) + return index; + index++; + } + } + return -1; +} +``` + +### 7、LinkedList的迭代器 + +在LinkedList中除了有一个Node的内部类外,应该还能看到另外两个内部类,那就是ListItr,还有一个是DescendingIterator。 + +【ListItr内部类】 + +```java +private class ListItr implements ListIterator { +} +``` + +看一下他的继承结构,发现只继承了一个ListIterator,到ListIterator中一看: + +![image-20210330232202554](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330232202554.png) + +看到方法名之后,就发现不止有向后迭代的方法,还有向前迭代的方法,所以我们就知道了这个ListItr 这个内部类干嘛用的了,就是能让linkedList不光能像后迭代,也能向前迭代。 + +看一下ListItr中的方法,可以发现,在迭代的过程中,还能移除、修改、添加值得操作。 + +![image-20210330232338517](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210330232338517.png) + +【DescendingIterator内部类】 + +```java +private class DescendingIterator implements Iterator { + //看一下这个类,还是调用的ListItr,作用是封装一下Itr中几个方法,让使用者以正常的思维去写代码,例如,在从后往前遍历的时候,也是跟从前往后遍历一样,使用next等操作,而不用使用特殊的previous。 + private final ListItr itr = new ListItr(size()); + public boolean hasNext() { + return itr.hasPrevious(); + } + public E next() { + return itr.previous(); + } + public void remove() { + itr.remove(); + } +} +``` + +## 总结 + +1. linkedList本质上是一个双向链表,通过一个Node内部类实现的这种链表结构 +2. 能存储null值 +3. 跟arrayList相比较,就真正的知道了,LinkedList在删除和增加等操作上性能好,而ArrayList在查询的性能上好 +4. . 从源码中看,它不存在容量不足的情况 +5. . linkedList不光能够向前迭代,还能像后迭代,并且在迭代的过程中,可以修改值、添加值、还能移除值。 +6. linkedList不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。 + +代码在 `LinkedList源码分析`中`3、类的属性` \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/04.Vevtor\345\222\214Stack.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/04.Vevtor\345\222\214Stack.md" new file mode 100644 index 00000000..01c04a4a --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/04.Vevtor\345\222\214Stack.md" @@ -0,0 +1,240 @@ +--- +title: Vevtor和Stack +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/Vevtor-Stack/ +categories: + - java + - java-se + - 集合框架 +--- + +# Vevtor和Stack + +前面写了一篇关于的是LinkedList的除了它的数据结构稍微有一点复杂之外,其他的都很好理解的。这 一篇说的大家在开发中很少去用到,有的时候也可能是会用到的,了解就行。 + +注意在学习这一篇之前,需要有多线程的知识: + +**1)锁机制:对象锁、方法锁、类锁** + +对象锁就是方法锁:就是在一个类中的方法上加上synchronized关键字,这就是给这个方法加锁了。 + +类锁:锁的是整个类,当有多个线程来声明这个类的对象的时候将会被阻塞,直到拥有这个类锁的对象被销毁或者主动释放了类锁。这个时候在被阻塞住的线程被挑选出一个占有该类锁,声明该类的对象。 其他线程继续被阻塞住。例如:在类A上有关键字synchronized,那么就是给类A加了类锁,线程1第一 个声明此类的实例,则线程1拿到了该类锁,线程2在想声明类A的对象,就会被阻塞。 + +2)在本文中,使用的是方法锁。 + +3)每个对象只有一把锁,有线程A,线程B,还有一个集合C类,线程A操作C拿到了集合中的锁(在集合C中有用synchronized关键字修饰的),并且还没有执行完,那么线程A就不会释放锁,当轮到线程B去操作集合C中的方法时 ,发现锁被人拿走了,所以线程B只能等待那个拿到锁的线程使用完,然后才能拿到锁进行相应的操作。 + +### 1、Vector概述 + +![image-20210331092950219](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331092950219.png) + + + +通过API中可以知道: + +1. Vector是一个可变化长度的数组 +2. Vector增加长度通过的是capacity和capacityIncrement这两个变量,目前还不知道如何实现自动 扩增的,等会源码分析 +3. Vector也可以获得iterator和listIterator这两个迭代器,并且他们发生的是fail-fast,而不是failsafe,注意这里,不要觉得这个vector是线程安全就搞错了,具体分析在下面会说 +4. Vector是一个线程安全的类,如果使用需要线程安全就使用Vector,如果不需要,就使用arrayList +5. Vector和ArrayList很类似,就少许的不一样,从它继承的类和实现的接口来看,跟arrayList一模一 样。 + +注意:java1.5推出的java.uitl.concurrent包,为了解决复杂的并发问题的。所以开发中,不建议用vector,原因在文章的结束会有解释,需要线程安全的集合类直接用java.util.concurrent包下的类。 + +### 2、Vector源码分析 + +```java +public class Vector + extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable + { + + } +``` + +我们发现Vector的继承关系和层次结构和ArrayList中的一模一样,忘记的可以去ArrayList标题查看! + +#### 构造方法 + +一共有四个构造方法。 + +构造方法作用: + +- 初始化存储元素的容器,也就是数组,elementData, +- 初始化capacityIncrement的大小,默认是0,这个的作用就是扩展数组的时候,增长的大小,为0 则每次扩展2倍 + +Vector():空构造 + +```java +/** + * Constructs an empty vector so that its internal data array + * has size {@code 10} and its standard capacity increment is + * zero. + */ +///看注释,这个是一个空的Vector构造方法,所以让他使用内置的数组,这里还不知道什么是内置的数组,看它调用了自身另外一个带一个参数的构造器 +public Vector() { + this(10); +} +``` + +Vector(int) + +```java +/** + * Constructs an empty vector with the specified initial capacity and + * with its capacity increment equal to zero. + * + * @param initialCapacity the initial capacity of the vector + * @throws IllegalArgumentException if the specified initial capacity + * is negative + */ +//注释说,给空的cector构造器用和带有一个特定初始化容量用的,并且又调用了另外一个带两个参数的构造器,并且给容量增长值(capacityIncrement=0)为0,查看vector中的变量可以发现capacityIncrement是一个成员变量 + +public Vector(int initialCapacity) { + this(initialCapacity, 0); +} +``` + +ector(int,int) + +```java +/** + * Constructs an empty vector with the specified initial capacity and + * capacity increment. + * + * @param initialCapacity the initial capacity of the vector + * @param capacityIncrement the amount by which the capacity is + * increased when the vector overflows + * @throws IllegalArgumentException if the specified initial capacity + * is negative + */ +//构建一个有特定的初始化容量和容量增长值的空的Vector, +public Vector(int initialCapacity, int capacityIncrement) { + super();//调用父类的构造,是个空构造 + if (initialCapacity < 0)//小于0,会报非法参数异常:不合法的容量 + throw new IllegalArgumentException("Illegal Capacity: "+ + initialCapacity); + this.elementData = new Object[initialCapacity];//elementData是一个成员变量数组,初始化它,并给它初始化长度。默认就是10,除非自己给值。 + this.capacityIncrement = capacityIncrement;//capacityIncrement的意思是如果要扩增数组,每次增长该值,如果该值为0,那数组就变为两倍的原长度,这个之后会分析到 +} +``` + +Vector(Collection c) + +```java +/** + * Constructs a vector containing the elements of the specified + * collection, in the order they are returned by the collection's + * iterator. + * + * @param c the collection whose elements are to be placed into this + * vector + * @throws NullPointerException if the specified collection is null + * @since 1.2 + */ +//将集合c变为Vector,返回Vector的迭代器。 +public Vector(Collection c) { + elementData = c.toArray(); + elementCount = elementData.length; + // c.toArray might (incorrectly) not return Object[] (see 6260652) + if (elementData.getClass() != Object[].class) + elementData = Arrays.copyOf(elementData, elementCount, Object[].class); +} +``` + +#### 核心方法 + +add()方法 + +```java +/** + * Appends the specified element to the end of this Vector. + * + * @param e element to be appended to this Vector + * @return {@code true} (as specified by {@link Collection#add}) + * @since 1.2 + */ + +//就是在vector中的末尾追加元素。但是看方法,synchronized,明白了为什么vector是线程安全的,因为在方法前面加了synchronized关键字,给该方法加锁了,哪个线程先调用它,其它线程就得等着,如果不清楚的就去看看多线程的知识,到后面我也会一一总结的。 +public synchronized boolean add(E e) { + modCount++; + //通过arrayList的源码分析经验,这个方法应该是在增加元素前,检查容量是否够用 + ensureCapacityHelper(elementCount + 1); + elementData[elementCount++] = e; + return true; +} +``` + +ensureCapacityHelper(int) + +```java +/** + * This implements the unsynchronized semantics of ensureCapacity. + * Synchronized methods in this class can internally call this + * method for ensuring capacity without incurring the cost of an + * extra synchronization. + * + * @see #ensureCapacity(int) + */ +////这里注释解释,这个方法是异步(也就是能被多个线程同时访问)的,原因是为了让同步方法都能调用到这个检测容量的方法,比如add的同时,另一个线程调用了add的重载方法,那么两个都需要同时查询容量够不够,所以这个就不需要用synchronized修饰了。因为不会发生线程不安全的问题 +private void ensureCapacityHelper(int minCapacity) { + // overflow-conscious code + if (minCapacity - elementData.length > 0) + //容量不够,就扩增,核心方法 + grow(minCapacity); +} +``` + +grow(int) + +```java +//看一下这个方法,其实跟arrayList一样,唯一的不同就是在扩增数组的方式不一样,如果capacityIncrement不为0,那么增长的长度就是capacityIncrement,如果为0,那么扩增为2倍的原容量 + +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + ((capacityIncrement > 0) ? + capacityIncrement : oldCapacity); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + +只要能看的懂ArrayList,这个就是在每个方法上比arrayList多了一个synchronized,其他都一 样。这里就不再分析了! + +```java +public synchronized E get(int index) { + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + + return elementData(index); +} +``` + +### 3、Stack + +现在来看看Vector的子类Stack,学过数据结构都知道,这个就是栈的意思。那么该类就是跟栈的用法一 样了 + +```java +class Stack extends Vector {} +``` + +通过查看他的方法,和查看api文档,很容易就能知道他的特性。就几个操作,出栈,入栈等,构造方法也是空的,用的还是数组,父类中的构造,跟父类一样的扩增方式,并且它的方法也是同步的,所以也是线程安全。 + +![image-20210331133023382](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331133023382.png) + +### 4、总结Vector和Stack + +【Vector总结(通过源码分析)】 + +1. Vector线程安全是因为它的方法都加了synchronized关键字 +2. Vector的本质是一个数组,特点能是能够自动扩增,扩增的方式跟capacityIncrement的值有关 +3. 它也会fail-fast,还有一个fail-safe两个的区别在下面的list总结中会讲到。 + +【Stack的总结】 + +1. 对栈的一些操作,先进后出 +2. 底层也是用数组实现的,因为继承了Vector +3. 也是线程安全的 \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/05.List\346\200\273\347\273\223.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/05.List\346\200\273\347\273\223.md" new file mode 100644 index 00000000..e1a69b13 --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/05.List\346\200\273\347\273\223.md" @@ -0,0 +1,83 @@ +--- +title: List总结 +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/List-summary/ +categories: + - java + - java-se + - 集合框架 +--- + +## List总结 + +**arrayList和LinkedList区别** + + arrayList底层是用数组实现的顺序表,是随机存取类型,可自动扩增,并且在初始化时,数组的长度是0,只有在增加元素时,长度才会增加。默认是10,不能无限扩增,有上限,在查询操作的时候性能更好 + + LinkedList底层是用链表来实现的,是一个双向链表,注意这里不是双向循环链表,顺序存取类型。 在源码中,似乎没有元素个数的限制。应该能无限增加下去,直到内存满了在进行删除,增加操作时性能更好。 + +两个都是线程不安全的,在iterator时,会发生fail-fast:快速失效。 + + + +**arrayList和Vector的区别** + + arrayList线程不安全,在用iterator,会发生fail-fast + + Vector线程安全,因为在方法前加了Synchronized关键字。也会发生fail-fast + + + +**fail-fast和fail-safe区别和什么情况下会发生** + + 简单的来说:在java.util下的集合都是发生fail-fast,而在java.util.concurrent下的发生的都是failsafe。 + + 1)fail-fast + +快速失败,例如在arrayList中使用迭代器遍历时,有另外的线程对arrayList的存储数组进行了改变,比如add、delete、等使之发生了结构上的改变,所以Iterator就会快速报一个 java.util.ConcurrentModificationException 异常(并发修改异常),这就是快速失败。 + + 2)fail-safe + +安全失败,在java.util.concurrent下的类,都是线程安全的类,他们在迭代的过程中,如果有线程进行结构的改变,不会报异常,而是正常遍历,这就是安全失败。 + + 3)为什么在java.util.concurrent包下对集合有结构的改变,却不会报异常? + +在concurrent下的集合类增加元素的时候使用Arrays.copyOf()来拷贝副本,在副本上增加元素,如果有其他线程在此改变了集合的结构,那也是在副本上的改变,而不是影响到原集合,迭代器还是照常遍 历,遍历完之后,改变原引用指向副本,所以总的一句话就是如果在此包下的类进行增加删除,就会出现一个副本。所以能防止fail-fast,这种机制并不会出错,所以我们叫这种现象为fail-safe。 + + 4)vector也是线程安全的,为什么是fail-fast呢? + +这里搞清楚一个问题,并不是说线程安全的集合就不会报fail-fast,而是报fail-safe,你得搞清楚前面所说答案的原理,出现fail-safe是因为他们在实现增删的底层机制不一样,就像上面说的,会有一个副本,而像arrayList、linekdList、verctor等,他们底层就是对着真正的引用进行操作,所以才会发生异常。 + + 5)既然是线程安全的,为什么在迭代的时候,还会有别的线程来改变其集合的结构呢(也就是对其删除和增加等操作)? + +首先,我们迭代的时候,根本就没用到集合中的删除、增加,查询的操作,就拿vector来说,我们都没有用那些加锁的方法,也就是方法锁放在那没人拿,在迭代的过程中,有人拿了那把锁,我们也没有办法,因为那把锁就放在那边。 + + + +【举例说明fail-fast和fail-safe的区别】 + +- fail-fast + +![image-20210331135807175](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331135807175.png) + +- fail-safe + +通过CopyOnWriteArrayList这个类来做实验,不用管这个类的作用,但是他确实没有报异常, 并且还通过第二次打印,来验证了上面我们说创建了副本的事情。 + +原理是在添加操作时会创建副本,在副本上进行添加操作,等迭代器遍历结束后,会将原引用 改为副本引用,所以我们在创建了一个list的迭代器,结果打印的就是123444了, + +证明了确实改变成为了副本引用,后面为什么是三个4,原因是我们循环了3次,不就添加了3 个4吗。如果还感觉不爽的话,看下add的源码。 + +![image-20210331151011751](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331151011751.png) + +![image-20210331151024361](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331151024361.png) + +【为什么现在都不提倡使用vector了】 + +1)vector实现线程安全的方法是在每个操作方法上加锁,这些锁并不是必须要的,在实际开发中, 一般都是通过锁一系列的操作来实现线程安全,也就是说将需要同步的资源放一起加锁来保证线程安全。 + +2)如果多个Thread并发执行一个已经加锁的方法,但是在该方法中,又有vector的存在,vector本身实现中已经加锁了,那么相当于锁上又加锁,会造成额外的开销。 + +3)就如上面第三个问题所说的,vector还有fail-fast的问题,也就是说它也无法保证遍历安全,在遍历时又得额外加锁,又是额外的开销,还不如直接用arrayList,然后再加锁呢。 + +总结:Vector在你不需要进行线程安全的时候,也会给你加锁,也就导致了额外开销,所以在 jdk1.5之后就被弃用了,现在如果要用到线程安全的集合,都是从java.util.concurrent包下去拿相应的类。 \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/06.HashMap.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/06.HashMap.md" new file mode 100644 index 00000000..462689c9 --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/06.HashMap.md" @@ -0,0 +1,591 @@ +--- +title: HashMap +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/HashMap/ +categories: + - java + - java-se + - 集合框架 +--- + +## HashMap + +### HashMap引入 + +问题:建立学生学号和学生姓名间的键值映射,并通过key对value进行操作,应该如何实现数据的存储和操作呢? + + Map接口专门处理键值映射数据的存储,可以根据键实现对值的操作。 最常用的实现类是HashMap。 + +```java +public static void main(String[] args) { + Map map = new HashMap(); + map.put("004","李清照"); + map.put("001","李白"); + map.put("003","王羲之"); + map.put("002","杜甫"); + + System.out.println(map.get("003")); + + //获取所有key 值 + Set keySet = map.keySet(); + for (String s : keySet){ + String s1 = map.get(s); + System.out.println(s+" "+s1); + } + + //获取所有值 + Collection values = map.values(); + for (String s : values){ + System.out.println(s); + } + + //entrySet() 获取值 + Set> entrySet = map.entrySet(); + for (Map.Entry m : entrySet){ + String key = m.getKey(); + String value = m.getValue(); + System.out.println(key+","+value); + } +} +``` + +### HashMa数据结构 + +HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对映射。此类不保证映 射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能。 + +在API中给出了相应的定义: + +又到了最激动人心的源码分析环节**:smile:** + +![image-20210331153042083](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331153042083.png) + + 第一段: + +哈希表基于map接口的实现,这个实现提供了map所有的操作,并且提供了key和value,可以为 null,(HashMap和HashTable大致上是一样的,除了hashmap是异步的,和允许key和value为 null) + +这个类不确定map中元素的位置,特别要提的是,这个类也不确定元素的位置随着时间会不会保持不变。 + + 第二段: + +假设哈希函数将元素合适的分到了每个桶(其实就是指的数组中位置上的链表)中,则这个实现为基本的操作(get、put)提供了稳定的性能,迭代这个集合视图需要的时间跟hashMap实例(key-value映射的数量)的容量(在桶中)成正比,因此,如果迭代的性能很重要的话,就不要将初始容量设置的太高或者 loadfactor设置的太低,【这里的桶,相当于在数组中每个位置上放一个桶装元素】 + + 第三段: + +HashMap的实例有两个参数影响性能,初始化容量(initialCapacity)和loadFactor加载因子, 在哈希表中这个容量是桶的数量【也就是数组的长度】,一个初始化容量仅仅是在哈希表被创建时容量, 在容量自动增长之前加载因子是衡量哈希表被允许达到的多少的。当entry的数量在哈希表中超过了加载 因子乘以当前的容量,那么哈希表被修改(内部的数据结构会被重新建立)所以哈希表有大约两倍的桶的数量. + + 第四段: + +通常来讲,默认的加载因子(0.75)能够在时间和空间上提供一个好的平衡,更高的值会减少空间上的开支但是会增加查询花费的时间(体现在HashMap类中get、put方法上),当设置初始化容量时,应该考虑到map中会存放entry的数量和加载因子,以便最少次数的进行rehash操作,如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。 + + 第五段: + +如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的 容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。 + +#### HashMap在JDK1.8以前数据结构和存储原理 + +【链表散列】 + +首先我们要知道什么是链表散列?通过数组和链表结合在一起使用,就叫做链表散列。这其实就是 hashmap存储的原理图。 + +![image-20210331153544575](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331153544575.png) + +【HashMap的数据结构和存储原理】 + +HashMap的数据结构就是用的链表散列。那HashMap底层是怎么样使用这个数据结构进行数据存取的呢?分成两个部分: + +第一步:HashMap内部有一个entry的内部类,其中有四个属性,我们要存储一个值,则需要一个key 和一个value,存到map中就会先将key和value保存在这个Entry类创建的对象中。 + +```java +static class Entry implements Map.Entry { + final K key; //就是我们说的map的key + V value; //value值,这两个都不陌生 + Entry next;//指向下一个entry对象 + int hash;//通过key算过来的你hashcode值。 +} +``` + +Entry的物理模型图: + +![image-20210331154056812](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331154056812.png) + +第二步:构造好了entry对象,然后将该对象放入数组中,如何存放就是这hashMap的精华所在了。 + +大概的一个存放过程是:通过entry对象中的hash值来确定将该对象存放在数组中的哪个位置上,如果在这个位置上还有其他元素,则通过链表来存储这个元素。 + +![image-20210331154430064](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331154430064.png) + +【Hash存放元素的过程】 + +通过key、value封装成一个entry对象,然后通过key的值来计算该entry的hash值,通过entry的hash值和数组的长度length来计算出entry放在数组中的哪个位置上面, + +每次存放都是将entry放在第一个位置。在这个过程中,就是通过hash值来确定将该对象存放在数组中 的哪个位置上。 + +#### JDK1.8后HashMap的数据结构 + +![image-20210331160154030](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331160154030.png) + +上图很形象的展示了HashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率。 + +![hashMap内存结构图](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/20181227162958496) + +### HashMap的属性 + +HashMap的实例有两个参数影响其性能。 + +初始容量:哈希表中桶的数量 + +加载因子:哈希表在其容量自动增加之前可以达到多满,的一种尺度 + +当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行 rehash操作,将哈希表扩充至两倍的桶数。 + +Java中默认初始容量为16,加载因子为0.75。 + +```java +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +static final float DEFAULT_LOAD_FACTOR = 0.75f; +``` + +【loadFactor加载因子】 + +定义:loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为 0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。 + + loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据 (entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,那么数组中存放的数据也就越稀,也就是可能数组中每个位置上就放一个元素。 + +那有人说,就把loadFactor变为1最好吗,存的数据很多,但是这样会有一个问题,就是我们在通过key拿到我们的value时,是先通过key的hashcode值,找到对应数组中的位置,如果该位置中有很多元素,则需要通过equals来依次比较链表中的元素,拿到我们的value值,这样花费的性能就很高,如果能让数组上的每个位置尽量只有一个元素最好,我们就能直接得到value值了,所以有人又会说,那把loadFactor变得很小不就好了,但是如果变得太小,在数组中的位置就会太稀,也就是分散的太开,浪费很多空间,这样也不好,所以在hashMap 中loadFactor的初始值就是0.75,一般情况下不需要更改它。 + +```java +static final float DEFAULT_LOAD_FACTOR = 0.75f; +``` + +【桶】 + +根据前面画的HashMap存储的数据结构图,你这样想,数组中每一个位置上都放有一个桶,每个桶里 就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。 + +【capacity】 + +capacity译为容量代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数。默认值是 16。 + +一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂。 + +```java +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +``` + +【size的含义】 + + size就是在该HashMap的实例中实际存储的元素的个数 + +【threshold的作用】 + +```java +int threshold; +``` + + threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。 + +注意这里说的是考虑,因为实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条 件。 + +什么时候会扩增数组的大小?在put一个元素时先size>=threshold并且还要在对应数组位置上有元素, 这才能扩增数组。 + +我们通过一张HashMap的数据结构图来分析: + +![image-20210331160910195](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331160910195.png) + +### HashMap的源码分析 + +#### 1、HashMap的层次关系与继承结构 + +【HashMap继承结构】 + +![image-20210331161138894](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331161138894.png) + +上面就继承了一个abstractMap,也就是用来减轻实现Map接口的编写负担。 + +【实现接口】 + +```java +public class HashMap extends AbstractMap +implements Map, Cloneable, Serializable { + +} +``` + +Map:在AbstractMap抽象类中已经实现过的接口,这里又实现,实际上是多余的。但每个集合都有这样的错误,也没过大影响 + + Cloneable:能够使用Clone()方法,在HashMap中,实现的是浅层次拷贝,即对拷贝对象的改变会影响 被拷贝的对象。 + + Serializable:能够使之序列化,即可以将HashMap对象保存至本地,之后可以恢复状态。 + + + +#### 2、HashMap类的属性 + +```java +public class HashMap extends AbstractMap implements Map,Cloneable, Serializable { + // 序列号 + private static final long serialVersionUID = 362498820763181265L; + // 默认的初始容量是16 + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + // 最大容量 + static final int MAXIMUM_CAPACITY = 1 << 30; + // 默认的填充因子 + static final float DEFAULT_LOAD_FACTOR = 0.75f; + // 当桶(bucket)上的结点数大于这个值时会转成红黑树 + static final int TREEIFY_THRESHOLD = 8; + // 当桶(bucket)上的结点数小于这个值时树转链表 + static final int UNTREEIFY_THRESHOLD = 6; + // 桶中结构转化为红黑树对应的table的最小大小 + static final int MIN_TREEIFY_CAPACITY = 64; + // 存储元素的数组,总是2的幂次倍 + transient Node[] table; + // 存放具体元素的集 + transient Set> entrySet; + // 存放元素的个数,注意这个不等于数组的长度。 + transient int size; + // 每次扩容和更改map结构的计数器 + transient int modCount; + // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 + int threshold; + // 填充因子 + final float loadFactor; +} +``` + +#### 3、HashMap的构造方法 + +有四个构造方法,构造方法的作用就是记录一下16这个数给threshold(这个数值最终会当作第一次组的长度。)和初始化加载因子。注意,hashMap中table数组一开始就已经是个没有长度的数组了。 + +构造方法中,并没有初始化数组的大小,数组在一开始就已经被创建了,构造方法只做两件事情,一个 是初始化加载因子,另一个是用threshold记录下数组初始化的大小。注意是记录。 + +【HashMap()】 + +```java +//看上面的注释就已经知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75 +//初始化容量:也就是初始化数组的大小 +//加载因子:数组上的存放数据疏密程度。 + +public HashMap() { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); +} +``` + +【HashMap(int)】 + +```java +public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); +} +``` + +【HashMap(int,float)】 + +```java +public HashMap(int initialCapacity, float loadFactor) { + // 初始容量不能小于0,否则报错 + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + // 初始容量不能大于最大值,否则为最大值 + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + // 填充因子不能小于或等于0,不能为非数字 + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + // 初始化填充因子 + this.loadFactor = loadFactor; + // 初始化threshold大小 + this.threshold = tableSizeFor(initialCapacity); +} + +``` + +【HashMap(Map m) 】 + +```java +public HashMap(Map m) { + // 初始化填充因子 + this.loadFactor = DEFAULT_LOAD_FACTOR; + // 将m中的所有元素添加至HashMap中 + putMapEntries(m, false); +} +``` + +【putMapEntries(Map m, boolean evict)函数将m的所有元素存入本 +HashMap实例中】 + +```java + + /** + * Implements Map.putAll and Map constructor. + * + * @param m the map + * @param evict false when initially constructing this map, else + * true (relayed to method afterNodeInsertion). + */ + final void putMapEntries(Map m, boolean evict) { + int s = m.size(); + if (s > 0) { + // 判断table是否已经初始化 + if (table == null) { // pre-size + // 未初始化,s为m的实际元素个数 + float ft = ((float)s / loadFactor) + 1.0F; + int t = ((ft < (float)MAXIMUM_CAPACITY) ? + (int)ft : MAXIMUM_CAPACITY); + // 计算得到的t大于阈值,则初始化阈值 + if (t > threshold) + threshold = tableSizeFor(t); + } + // 已初始化,并且m元素个数大于阈值,进行扩容处理 + else if (s > threshold) + resize(); + // 将m中的所有元素添加至HashMap中 + for (Map.Entry e : m.entrySet()) { + K key = e.getKey(); + V value = e.getValue(); + putVal(hash(key), key, value, false, evict); + } + } + } +``` + +#### 4、HashMap常用方法 + +【put(K key, V value)】 + +```java +public V put(K key, V value) { +return putVal(hash(key), key, value, false, true); +} +``` + +【putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)】 + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + // table未初始化或者长度为0,进行扩容 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + // 桶中已经存在元素 + else { + Node e; K k; + // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + // 将第一个元素赋值给e,用e来记录 + e = p; + // hash值不相等,即key不相等;为红黑树结点 + else if (p instanceof TreeNode) + // 放入树中 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + // 为链表结点 + else { + // 在链表最末插入结点 + for (int binCount = 0; ; ++binCount) { + // 到达链表的尾部 + if ((e = p.next) == null) { + // 在尾部插入新结点 + p.next = newNode(hash, key, value, null); + // 结点数量达到阈值,转化为红黑树 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + // 跳出循环 + break; + } + // 判断链表中结点的key值与插入的元素的key值是否相等 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + // 相等,跳出循环 + break; + // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 + p = e; + } + } + // 表示在桶中找到key值、hash值与插入元素相等的结点 + if (e != null) { // existing mapping for key + // 记录e的value + V oldValue = e.value; + // onlyIfAbsent为false或者旧值为null + if (!onlyIfAbsent || oldValue == null) + //用新值替换旧值 + e.value = value; + // 访问后回调 + afterNodeAccess(e); + // 返回旧值 + return oldValue; + } + } + // 结构性修改 + ++modCount; + // 实际大小大于阈值则扩容 + if (++size > threshold) + resize(); + // 插入后回调 + afterNodeInsertion(evict); + return null; +} +``` + +HashMap并没有直接提供putVal接口给用户调用,而是提供的put函数,而put函数就是通过putVal来插入元素的。 + +【get(Object key)】 + +```java +public V get(Object key) { + Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} +``` + +【getNode(int hash,Pbject key)】 + +```java +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + // table已经初始化,长度大于0,根据hash寻找table中的项也不为空 + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + // 桶中第一项(数组元素)相等 + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + // 桶中不止一个结点 + if ((e = first.next) != null) { + // 为红黑树结点 + if (first instanceof TreeNode) + // 在红黑树中查找 + return ((TreeNode)first).getTreeNode(hash, key); + // 否则,在链表中查找 + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + + HashMap并没有直接提供getNode接口给用户调用,而是提供的get函数,而get函数就是通过 getNode来取得元素的。 + + + +【resize方法】 + +```java +final Node[] resize() { + // 当前table保存 + Node[] oldTab = table; + // 保存table大小 + int oldCap = (oldTab == null) ? 0 : oldTab.length; + // 保存当前阈值 + int oldThr = threshold; + int newCap, newThr = 0; + // 之前table大小大于0 + if (oldCap > 0) { + // 之前table大于最大容量 + if (oldCap >= MAXIMUM_CAPACITY) { + // 阈值为最大整形 + threshold = Integer.MAX_VALUE; + return oldTab; + } + // 容量翻倍,使用左移,效率更高 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + // 阈值翻倍 + newThr = oldThr << 1; // double threshold + } + // 之前阈值大于0 + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + // oldCap = 0并且oldThr = 0,使用缺省值(如使用HashMap()构造函数,之后再插入一个元素会调用resize函数,会进入这一步) + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 新阈值为0 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + @SuppressWarnings({"rawtypes","unchecked"}) + // 初始化table + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; + // 之前的table已经初始化过 + if (oldTab != null) { + // 复制元素,重新进行hash + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + oldTab[j] = null; + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + else { // preserve order + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + // 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} +``` + +进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。 + +在resize前和resize后的元素布局如下: + +![image-20210331163600275](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331163600275.png) + +上图只是针对了数组下标为2的桶中的各个元素在扩容后的分配布局,其他各个桶中的元素布局可以以此类推。 + +#### 5、总结 + +【关于数组扩容】 + +从putVal源代码中我们可以知道,当插入一个元素的时候size就加1,若size大于threshold的时候,就会进行扩容。假设我们的capacity大小为32,loadFator为0.75,则threshold为24 = 32 * 0.75, + +此时,插入了25个元素,并且插入的这25个元素都在同一个桶中,桶中的数据结构为红黑树,则还 有31个桶是空的,也会进行扩容处理,其实,此时,还有31个桶是空的,好像似乎不需要进行扩容处 理,但是是需要扩容处理的,因为此时我们的capacity大小可能不适当。我们前面知道,扩容处理会遍 历所有的元素,时间复杂度很高;前面我们还知道,经过一次扩容处理后,元素会更加均匀的分布在各 个桶中,会提升访问效率。所以,说尽量避免进行扩容处理,也就意味着,遍历元素所带来的坏处大于 元素在桶中均匀分布所带来的好处。 + +【总结】 + +1. 要知道hashMap在JDK1.8以前是一个链表散列这样一个数据结构,而在JDK1.8以后是一个数组加 链表加红黑树的数据结构。 +2. 通过源码的学习,hashMap是一个能快速通过key获取到value值得一个集合,原因是内部使用的 是hash查找值得方法。 \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/07.Set.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/07.Set.md" new file mode 100644 index 00000000..0fe2712e --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/07.Set.md" @@ -0,0 +1,51 @@ +--- +title: set +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/set/ +categories: + - java + - java-se + - 集合框架 +--- + +## Set + +Set注重独一无二的性质,该体系集合可以知道某物是否已经存在于集合中,不会存储重复的元素,用于存储无序**(存入和取出的顺序不一定相同)**元素,值不能重复 + +对象的相等性: +引用到堆上同一个对象的两个引用是相等的。如果对两个引用调用hashcode方法,会得到相同的结果,如果对象所属的类没有覆盖object的hashcode方法的话,hashcode会返回每个对象特有的序号(java是依据对象的内存地址计算出的此序号),所以两个不同的对象的hashcode值是不可能相等的。 + +如果想要让两个不同的Person对象视为相等的,就必须覆盖Object继承下来的hashcode方法和equals方法,因为Object hashcode返回的是该对象的内存地址,所以必须重写hashcode方法,才能保证两个不同的对象具有相同的hashcode,同时也需要两个不同对象比较equals方法返回true。 + +该集合中没有特有的方法,直接继承自Collection + +```java +/** + * Collection + * \--List + * 有序(存储顺序和取出顺序一致),可重复 + * \--Set + * 无序(存储顺序和取出顺序不一致),唯一 + * HashSet:它不保证set的迭代顺序;特别是它不保证该顺序恒久不变 + * 注意:虽然set集合的元素无序,但是,作为集合来说,它肯定有它自己的存储顺序, + * 而你的顺序恰巧和它的存储顺序一致,这代表不了有序,你可以多存储一些数据就能看到效果 + **/ +``` + +案例:set集合添加元素并使用增强for循环遍历 + +```java +public static void method1() { + Set set = new HashSet<>(); + set.add("1"); + set.add("5"); + set.add("2"); + + set.add("5");//重复的不会添加进去 + for (String s : set) { + System.out.println(s); + } +} +``` + +最后输出顺序是: 1、2、5 \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/08.HashSet.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/08.HashSet.md" new file mode 100644 index 00000000..e3612790 --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/08.HashSet.md" @@ -0,0 +1,125 @@ +--- +title: HashSet +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/HashSet/ +categories: + - java + - java-se + - 集合框架 +--- + +## HashSet + +HashSet是一个没有重复元素的集合,它其实是由HashMap实现的,HashMap保存的是建值对,然而我们只能向HashSet中添加Key,原因在于HashSet的Value其实都是同一个对象,这是HashSet添加元素的方法,可以看到辅助实现HashSet的map中的value其实都是Object类的同一个对象。 + +特点: + +- 底层数据结构是哈希表 +- 对集合的迭代顺序不作任何保证,也就是说不保证存储和取出的元素顺序一致 +- 没有带索引的方法,所以不能使用普通for循环遍历 +- 由于是Set集合,所以是不包含重复元素的集合 + + + +### 存储规则 + +**哈希表边存放的是哈希值。**HashSet存储元素的顺序并不是按照存入时的顺序(和List显然不同) 是按照哈希值来存的所以取数据也是按照哈希值取得。 + +HashSet不存入重复元素的规则.使用hashcode和equals + +由于Set集合是不能存入重复元素的集合。那么HashSet也是具备这一特性的。HashSet如何检查重复?HashSet会通过元素的hashcode()和equals方法进行判断元素师否重复。 + +当你试图把对象加入HashSet时,HashSet会使用对象的hashCode来判断对象加入的位置。同时也会与其他已经加入的对象的hashCode进行比较,如果没有相等的hashCode,HashSet就会假设对象没有重复出现。 + +简单一句话,如果对象的hashCode值是不同的,那么HashSet会认为对象是不可能相等的。 + +因此我们自定义类的时候需要重写hashCode,来确保对象具有相同的hashCode值。 + +如果元素(对象)的hashCode值相同,是不是就无法存入HashSet中了?当然不是,会继续使用equals 进行比较。如果 equals为true 那么HashSet认为新加入的对象重复了,所以加入失败。如果equals 为false那么HashSet 认为新加入的对象没有重复,新元素可以存入。 + +**总结:** + +元素的哈希值是通过元素的hashcode方法来获取的, HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法 如果 equls结果为true ,HashSet就视为同一个元素。如果equals 为false就不是同一个元素。 + +哈希值相同equals为false的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。 +![image-20210331235903905](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331235903905.png) + +### HashSet用处 + +问题:现在有一批数据,要求不能重复存储元素,而且要排序。ArrayList 、 LinkedList不能去除重复数据。HashSet可以去除重复,但是是无序。 + +所以这时候就要使用TreeSet了 + +案例:创建一个学生类,并重写equals和hashcode + +```java +import java.util.Objects; + +public class HashSetStudent { + public String name; + public int age; + + public HashSetStudent(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HashSetStudent that = (HashSetStudent) o; + return age == that.age && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } +} +``` + +代码: + +```java +public class HashSetTest { + public static void main(String[] args) { + HashSetStudent s1 = new HashSetStudent("李白1",15); + HashSetStudent s2 = new HashSetStudent("李白2",16); + + HashSetStudent s3 = new HashSetStudent("李白1",15);//重复值不插入 + + HashSetStudent s4 = new HashSetStudent("李白1",10); + HashSetStudent s5 = new HashSetStudent("李白2",10); + + HashSet hs = new HashSet<>(); + hs.add(s1); + hs.add(s2); + hs.add(s3); + hs.add(s4); + hs.add(s5); + + for(HashSetStudent S:hs){ + System.out.println(S.name+"--"+S.age); + } + + } +} +``` + +最后输出: + +> 李白2--10 +> 李白1--10 +> 李白1--15 +> 李白2--16 + +### LinkedHashSet + +特点: + +- 哈希表和链表实现的Set接口,具有可预测的迭代次序 +- 由链表保证元素有序,也就是说元素的存储和取出顺序是一致的 +- 由哈希表保证元素唯一,也就是说没有重复的元素 + +待补充...... \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/09.TreeSet.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/09.TreeSet.md" new file mode 100644 index 00000000..2b95af10 --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/09.TreeSet.md" @@ -0,0 +1,202 @@ +--- +title: TreeSet +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/TreeSet/ +categories: + - java + - java-se + - 集合框架 +--- + +## TreeSet + +**TreeSet简介** + +红-黑树的数据结构,默认对元素进行自然排序 + +TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet, Cloneable, java.io.Serializable接口。 + +TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。 + +TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。 + +TreeSet 实现了Cloneable接口,意味着它能被克隆。 + +TreeSet 实现了java.io.Serializable接口,意味着它支持序列化。 + +TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。 + +TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。 +另外,TreeSet是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。 + +**特点:** + +- 元素有序,这里的顺序不是指存储和取出的顺序,而是按照一定的规则进行排序,具体排序方式取决于构造方法 + + TreeSet():根据其元素的自然非序进行排序 + + TreeSet(Comparator comparator):根据指定的比较器进行排序 + +- 没有带索引的方法,所以不能使用普通for循环遍历 + +- 由于是Set集合,所以不包含重复元素的集合 + + + +### TreeSet自然顺序 + +即类要实现Comparable接口,并重写compareTo()方法,TreeSet对象调用add()方法时,会将存入的对象提升为Comparable类型,然后调用对象中的compareTo()方法进行比较,根据比较的返回值进行存储。 + +因为TreeSet底层是二叉树,当compareTo方法返回0时,不存储;当compareTo方法返回正数时,存入二叉树的右子树;当compareTo方法返回负数时,存入二叉树的左子树。如果一个类没有实现Comparable接口就将该类对象存入TreeSet集合,会发生类型转换异常。 + + +### TreeSet自定义排序 + +方式一:元素自身具备比较性 + +元素自身具备比较性,需要元素实现Comparable接口,重写compareTo方法,也就是让元素自身具备比较性,这种方式叫做元素的自然排序也叫做默认排序。 + +**让元素自身具备比较性** + + + +也就是元素需要实现Comparable接口,覆盖compareTo 方法。 + +案例:创建Student类,有姓名,年龄。存入集合后,先根据年龄大小,再根据姓名来进行排序插入集合中 + +```java +public class Student implements Comparable { + public String name; + public int age; + + public Student(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public int compareTo(Student o) { + int i = this.age-o.age; + int n = i==0?this.name.compareTo(o.name):i; + return n; + } +} +``` + +重写compareTo()方法,返回值有三种情况 + +1. 返回值为0:不插入集合 +2. 返回值为1:往后插入集合 +3. 返回值为-1:往前插入集合 + +```java +public class Demo { + public static void main(String[] args) { + Student s1 = new Student("xishi",25); + Student s2 = new Student("yangyuhuan",29); + Student s3 = new Student("diaochan",28); + Student s4 = new Student("wangzhaojun",30); + Student s5 = new Student("libai",30); + Student s6 = new Student("libai",30); + + TreeSet ts = new TreeSet<>(); + + ts.add(s1); + ts.add(s2); + ts.add(s3); + ts.add(s4); + ts.add(s5); + ts.add(s6); + + for(Student s : ts){ + System.out.println(s.name+"---"+s.age); + } + + } +} +``` + + + +**让容器自身具备比较性,自定义比较器。** + +需求:当元素自身不具备比较性,或者元素自身具备的比较性不是所需的。 + +那么这时只能让容器自身具备。 + +定义一个类实现Comparator 接口,覆盖compare方法。 + +并将该接口的子类对象作为参数传递给TreeSet集合的构造函数。 + +当Comparable比较方式,及Comparator比较方式同时存在,以Comparator比较方式为主。 + + +```java +public class Demo5 { + public static void main(String[] args) { + TreeSet ts = new TreeSet(new MyComparator()); + ts.add(new Book("think in java", 100)); + ts.add(new Book("java 核心技术", 75)); + ts.add(new Book("现代操作系统", 50)); + ts.add(new Book("java就业教程", 35)); + ts.add(new Book("think in java", 100)); + ts.add(new Book("ccc in java", 100)); + + System.out.println(ts); + } +} + +class MyComparator implements Comparator { + + public int compare(Object o1, Object o2) { + Book b1 = (Book) o1; + Book b2 = (Book) o2; + System.out.println(b1+" comparator "+b2); + if (b1.getPrice() > b2.getPrice()) { + return 1; + } + if (b1.getPrice() < b2.getPrice()) { + return -1; + } + return b1.getName().compareTo(b2.getName()); + } + +} + +class Book { + private String name; + private double price; + + public Book() { + + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public Book(String name, double price) { + + this.name = name; + this.price = price; + } + + @Override + public String toString() { + return "Book [name=" + name + ", price=" + price + "]"; + } + +} +``` \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/10.\350\277\255\344\273\243\345\231\250.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/10.\350\277\255\344\273\243\345\231\250.md" new file mode 100644 index 00000000..9feb1653 --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/10.\350\277\255\344\273\243\345\231\250.md" @@ -0,0 +1,52 @@ +--- +title: 迭代器 +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/iterator/ +categories: + - java + - java-se + - 集合框架 +--- + +## 迭代器 + +所有实现了Collection接口的容器类都有一个iterator方法用以返回一个实现Iterator接口的对象 + +Iterator对象称作为迭代器,用以方便的对容器内元素的遍历操作,Iterator接口定义了如下方法: + +- boolean hashNext();//判断是否有元素没有被遍历 +- Object next();//返回游标当前位置的元素并将游标移动到下一个位置 +- void remove();//删除游标左边的元素,在执行完next之后该操作只能执行一次 + + + +**问题:如何遍历Map集合呢?** + + + +**方法1:通过迭代器Iterator实现遍历** + +获取Iterator :Collection 接口的iterator()方法 + +Iterator的方法: + +- boolean hasNext(): 判断是否存在另一个可访问的元素 +- Object next(): 返回要访问的下一个元素 + +```java +Set keys = dogMap.keySet(); //取出所有key的集合 +Iterator it = keys.iterator(); //获取Iterator对象 +while (it.hasNext()) { + String key = (String) it.next(); //取出key + Dog dog = (Dog) dogMap.get(key); //根据key取出对应的值 + System.out.println(key + "\t" + dog.getStrain()); +} +``` + +**方法2:增强for循环** + +```java +for(元素类型t 元素变量x : 数组或集合对象){ + 引用了x的java语句 +} +``` \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/11.\346\263\233\345\236\213.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/11.\346\263\233\345\236\213.md" new file mode 100644 index 00000000..24a29bbb --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/11.\346\263\233\345\236\213.md" @@ -0,0 +1,31 @@ +--- +title: 泛型 +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/generic-paradigm/ +categories: + - java + - java-se + - 集合框架 +--- + +## 泛型 + +Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许 程序员在编译时检测到非法的类型。 + +**泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。** + +如何解决以下强制类型转换时容易出现的异常问题? + + List的get(int index)方法获取元素 + Map的get(Object key)方法获取元素 + Iterator的next()方法获取元素 + +分析:通过泛型 , JDK1.5使用泛型改写了集合框架中的所有接口和类 + +![image-20210331222202938](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331222202938.png) + +![image-20210331222216052](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331222216052.png) + + + +? 通配符: < ? > \ No newline at end of file diff --git "a/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/12.Collections\345\267\245\345\205\267\347\261\273.md" "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/12.Collections\345\267\245\345\205\267\347\261\273.md" new file mode 100644 index 00000000..af704147 --- /dev/null +++ "b/docs/01.Java/05.Java-\351\233\206\345\220\210\346\241\206\346\236\266/12.Collections\345\267\245\345\205\267\347\261\273.md" @@ -0,0 +1,484 @@ +--- +title: Collections工具类 +date: 2021-04-16 16:19:25 +permalink: /java/se/collection/collections/ +categories: + - java + - java-se + - 集合框架 +--- + +## Collections工具类 + +【前言】 + +Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类提供了大量方法对集合进 行排序、查询和修改等操作,还提供了将集合对象置为不可变、对集合对象实现同步控制等方法。 + +这个类不需要创建对象,内部提供的都是静态方法。 + +### 1、Collectios概述 + +![image-20210331222908814](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331222908814.png) + +此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。它包含在 collection 上操作的多态算法,即“包装器”,包装器返回由指定 collection 支持的新 collection,以及少数其他内容。如果为 此类的方法所提供的 collection 或类对象为 null,则这些方法都将抛出 NullPointerException 。 + + + +### 2、排序操作 + +```java +static void reverse(List list)//反转列表中元素的顺序。 + +static void shuffle(List list) //对List集合元素进行随机排序。 + +static void sort(List list) //根据元素的自然顺序 对指定列表按升序进行排序 + +static void sort(List list, Comparator c) //根据指定比较器产生的顺序对指定列表进行排序。 + +static void swap(List list, int i, int j) //在指定List的指定位置i,j处交换元素。 + +static void rotate(List list, int distance) + //当distance为正数时,将List集合的后distance个元素“整体”移到前面;当distance为负数时,将list集合的前distance个元素“整体”移到后边。该方法不会改变集合的长度。 +``` + +【演示】 + +```java +public static void main(String[] args) { + ArrayList list = new ArrayList(); + list.add(3); + list.add(-2); + list.add(9); + list.add(5); + list.add(-1); + list.add(6); + + //输出:[3, -2, 9, 5, -1, 6] + System.out.println(list); + //集合元素的次序反转 + Collections.reverse(list); + //输出:[6, -1, 5, 9, -2, 3] + System.out.println(list); + //排序:按照升序排序 + Collections.sort(list); + //[-2, -1, 3, 5, 6, 9] + System.out.println(list); + //根据下标进行交换 + Collections.swap(list, 2, 5); + //输出:[-2, -1, 9, 5, 6, 3] + System.out.println(list); + + /*//随机排序 + Collections.shuffle(list); + //每次输出的次序不固定 + System.out.println(list);*/ + + //后两个整体移动到前边 + Collections.rotate(list, 2); + //输出:[6, 9, -2, -1, 3, 5] + System.out.println(list); +} +``` + +【演示】 + +创建学生集合,加入数据,并**自定义排序**,先根据年龄,再根据首字母 + +pojo类 + +```java +public class Student { + public String name; + public int age; + + public Student(String name, int age) { + this.name = name; + this.age = age; + } +} +``` + +test代码 + +```java +public static void main(String[] args) { + ArrayList array = new ArrayList<>(); + Student s1 = new Student("lingqingxia",20); + Student s2 = new Student("wangxizhi",30); + Student s3 = new Student("libai",25); + Student s4 = new Student("dufu",25); + //Student s5 = new Student("dufu",25); + + array.add(s1); + array.add(s2); + array.add(s3); + array.add(s4); + //array.add(s5); + Collections.sort(array, new Comparator() { + @Override + public int compare(Student o1, Student o2) { + int i = o1.age-o2.age; + int n = i==0?o1.name.compareTo(o2.name):i; + return n; + } + }); + + for (Student s:array){ + System.out.println(s.name+","+s.age); + } +} +``` + +### 3、查找、替换操作 + +【方法】 + +```java +//使用二分搜索法搜索指定列表,以获得指定对象在List集合中的索引。 +//注意:此前必须保证List集合中的元素已经处于有序状态。 +static int binarySearch(List> list, T key) + +//根据元素的自然顺序,返回给定collection 的最大元素。 +static Object max(Collection coll) + + //根据指定比较器产生的顺序,返回给定 collection 的最大元素。 + static Object max(Collection coll,Comparator comp) + + //根据元素的自然顺序,返回给定collection 的最小元素。 + static Object min(Collection coll) + + //根据指定比较器产生的顺序,返回给定 collection 的最小元素。 + static Object min(Collection coll,Comparator comp) + + 使用指定元素替换指定列表中的所有元素。 + static void fill(List list, T obj) + + //返回指定 collection 中等于指定对象的出现次数。 + static int frequency(Collection c, Object o) + + //返回指定源列表中第一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1。 + static int indexOfSubList(List source, List target) + + //返回指定源列表中最后一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1。 + static int lastIndexOfSubList(List source, List target) + + //使用一个新值替换List对象的所有旧值oldVal + static boolean replaceAll(List list, T oldVal, T newVal) + +``` + +【演示:实例使用查找、替换操作】 + +```java +public static void main(String[] args) { + ArrayList list = new ArrayList(); + list.add(3); + list.add(-2); + list.add(9); + list.add(5); + list.add(-1); + list.add(6); + //[3, -2, 9, 5, -1, 6] + System.out.println(list); + //输出最大元素9 + System.out.println(Collections.max(list)); + //输出最小元素:-2 + System.out.println(Collections.min(list)); + //将list中的-2用1来代替 + System.out.println(Collections.replaceAll(list, -2, 1)); + //[3, 1, 9, 5, -1, 6] + System.out.println(list); + list.add(9); + //判断9在集合中出现的次数,返回2 + System.out.println(Collections.frequency(list, 9)); + //对集合进行排序 + Collections.sort(list); + //[-1, 1, 3, 5, 6, 9, 9] + System.out.println(list); + //只有排序后的List集合才可用二分法查询,输出2 + System.out.println(Collections.binarySearch(list, 3)); +} +``` + +### 4、同步控制 + +Collectons提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从 而解决多线程并发访问集合时的线程安全问题。 + +正如前面介绍的HashSet,TreeSet,arrayList,LinkedList,HashMap,TreeMap都是线程不安全的。 Collections提供了多个静态方法可以把他们包装成线程同步的集合。 + +【方法】 + +```java +//返回指定 collection 支持的同步(线程安全的)collection。 +static Collection synchronizedCollection(Collection c) + +//返回指定列表支持的同步(线程安全的)列表。 +static List synchronizedList(List list) + +//返回由指定映射支持的同步(线程安全的)映射。 +static Map synchronizedMap(Map m) + +//返回指定 set 支持的同步(线程安全的)set +static Set synchronizedSet(Set s) +``` + +【实例】 + +```java +public static void main(String[] args) { + //下面程序创建了四个同步的集合对象 + Collection c = Collections.synchronizedCollection(new ArrayList()); + List list = Collections.synchronizedList(new ArrayList()); + Set s = Collections.synchronizedSet(new HashSet()); + Map m = Collections.synchronizedMap(new HashMap()); + +} +``` + +### 5、Collesction设置不可变集合 + +【方法】 + +```java +//返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map。 +emptyXxx() + +//返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。 +singletonXxx() + +//返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。 +unmodifiableXxx(): +``` + +上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本。 + +【实例】 + +```java +public static void main(String[] args) { + //创建一个空的、不可改变的List对象 + List unmodifiableList = Collections.emptyList(); + //unmodifiableList.add("java"); + //添加出现异常:java.lang.UnsupportedOperationException + System.out.println(unmodifiableList);// [] + //创建一个只有一个元素,且不可改变的Set对象 + Set unmodifiableSet = Collections.singleton("Struts2权威指南"); + //[Struts2权威指南] + System.out.println(unmodifiableSet); + //创建一个普通Map对象 + Map scores = new HashMap(); + scores.put("语文", 80); + scores.put("Java", 82); + //返回普通Map对象对应的不可变版本 + Map unmodifiableMap = Collections.unmodifiableMap(scores); + //下面任意一行代码都将引发UnsupportedOperationException异常 + unmodifiableList.add("测试元素"); + unmodifiableSet.add("测试元素"); + unmodifiableMap.put("语文", 90); + +} +``` + +### 总结和测试 + +实体类:Pojo + +```java +import java.util.*; + + +public class CollectionsTest { + public static void main(String[] args) { + //创建一个空的、不可改变的List对象 + List unmodifiableList = Collections.emptyList(); + //unmodifiableList.add("java"); + //添加出现异常:java.lang.UnsupportedOperationException + System.out.println(unmodifiableList);// [] + //创建一个只有一个元素,且不可改变的Set对象 + Set unmodifiableSet = Collections.singleton("Struts2权威指南"); + //[Struts2权威指南] + System.out.println(unmodifiableSet); + //创建一个普通Map对象 + Map scores = new HashMap(); + scores.put("语文", 80); + scores.put("Java", 82); + //返回普通Map对象对应的不可变版本 + Map unmodifiableMap = Collections.unmodifiableMap(scores); + //下面任意一行代码都将引发UnsupportedOperationException异常 + unmodifiableList.add("测试元素"); + unmodifiableSet.add("测试元素"); + unmodifiableMap.put("语文", 90); + + } + +} +``` + +测试类代码如下 + +```java +import java.util.ArrayList; +import java.util.List; + +public class Test01 { + public static void main(String[] args) throws Exception { + //一个对象对应了一行记录! + Employee e1 = new Employee(0301, "狂神", 3000, "项目部", "2017-10"); + Employee e2 = new Employee(0302, "小明", 3500, "教学部", "2016-10"); + Employee e3 = new Employee(0303, "小红", 3550, "教学部", "2016-10"); + List list = new ArrayList(); + list.add(e1); + list.add(e2); + list.add(e3); + printEmpName(list); + } + + public static void printEmpName(List list) { + for (int i = 0; i < list.size(); i++) { + System.out.println(list.get(i).getName() + "-" + list.get(i).getHireDate()); + } + } +} +``` + +### 斗地主案例 + +简易版本 + +```java +public static void main(String[] args) { + ArrayList array = new ArrayList<>(); + + String[] colors = {"方片", "梅花", "黑桃", "红桃"}; + String[] numbers = {"1","2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}; + + for (String c : colors) { + for (String n : numbers) { + array.add(c + n); + } + } + array.add("小王"); + array.add("大王"); + Collections.shuffle(array);//洗牌 + + //发牌 + ArrayList wj1 = new ArrayList<>();//玩家1 + ArrayList wj2 = new ArrayList<>(); + ArrayList wj3 = new ArrayList<>(); + ArrayList dp = new ArrayList<>();//底牌 + + + for (int i = 0; i < array.size(); i++) { + String s = array.get(i); + if (i >= array.size() - 3) { + dp.add(s); + }else{ + int i1 = i % 3; + switch (i1){ + case 0: + wj1.add(s); + break; + case 1: + wj2.add(s); + break; + case 2: + wj3.add(s); + break; + } + } + } + //看牌 + System.out.println("底牌:"+dp); + System.out.println("玩家1的牌"+wj1); + System.out.println("玩家2的牌"+wj2); + System.out.println("玩家3的牌"+wj3); + +} +``` + + +>底牌:[梅花1, 黑桃2, 红桃Q] +> +>玩家1的牌[黑桃10, 大王, 方片7, 梅花5, 方片9, 方片3, 黑桃4, 红桃8, 梅花4, 红桃9, 红桃2, 红桃4, 小王, 方片K, 红桃6, 黑桃6, 红桃K] +> +>玩家2的牌[红桃1, 红桃7, 黑桃7, 方片J, 红桃J, 梅花3, 梅花7, 梅花8, 梅花9, 梅花2, 梅花J, 红桃10, 方片10, 黑桃5, 方片1, 黑桃K, 黑桃Q] +> +>玩家3的牌[方片5, 方片4, 黑桃9, 方片8, 黑桃3, 方片Q, 方片6, 红桃5, 梅花6, 黑桃8, 黑桃1, 梅花Q, 红桃3, 梅花10, 方片2, 梅花K, 黑桃J] + +可以看到,能实现洗牌,发牌,看牌 + +但是牌的顺序不是从小到大的,我们来改进一下 + +![image-20210331233413189](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaSE-集合.assets/image-20210331233413189.png) + +1. 用HashMap键值对从0到53序号,存储牌 +2. 用ArrayList存牌的序号 +3. 用TreeSet存玩家的牌的序号,TreeSet可以自动排序 +4. 通过TreeSet的序号,从HashMap中查取牌 + +```java +public class Poker { + public static void main(String[] args) { + //编号,牌 + HashMap hm = new HashMap<>(); + //储存编号 + ArrayList array = new ArrayList<>(); + + String[] colors = {"方片", "梅花", "黑桃", "红桃"}; + String[] numbers = { "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K","A","2"}; + int index=0; + for (String c:colors){ + for (String n:numbers){ + hm.put(index,c+n); + array.add(index); + index++; + } + } + hm.put(index,"小王"); + array.add(index); + index++; + hm.put(index,"大王"); + array.add(index); + + Collections.shuffle(array);//洗牌 + //发牌 + TreeSet wj1 = new TreeSet<>();//玩家1 + TreeSet wj2 = new TreeSet<>(); + TreeSet wj3 = new TreeSet<>(); + TreeSet dp = new TreeSet<>();//底牌 + + for (int i = 0; i < array.size(); i++) { + Integer s = array.get(i); + if (i >= array.size() - 3) { + dp.add(s); + }else{ + int i1 = i % 3; + switch (i1){ + case 0: + wj1.add(s); + break; + case 1: + wj2.add(s); + break; + case 2: + wj3.add(s); + break; + } + } + } + lookpoke("玩家1",wj1 ,hm); + lookpoke("玩家2",wj2 ,hm); + lookpoke("玩家3",wj3 ,hm); + lookpoke("底牌",dp ,hm); + + } + public static void lookpoke(String name,TreeSet ts,HashMap hm){ + System.out.print(name+"的牌: "); + for (Integer t:ts){ + System.out.print(hm.get(t)+" "); + } + System.out.println(); + + } +} +``` \ No newline at end of file diff --git "a/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/02.JUC\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/02.JUC\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 00000000..028d800b --- /dev/null +++ "b/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/02.JUC\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,657 @@ +--- +title: JUC学习笔记(一) +permalink: /java/se/thread/study-note +date: 2021-05-15 18:09:11 +--- + +# JUC + + + + + +- [一 简介](#%E4%B8%80-%E7%AE%80%E4%BB%8B) + - [什么是JUC](#%E4%BB%80%E4%B9%88%E6%98%AFjuc) + - [进程和线程](#%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B) +- [二 Lock锁](#%E4%BA%8C-lock%E9%94%81) + - [synchronized锁](#synchronized%E9%94%81) + - [Lock 锁](#lock-%E9%94%81) + - [区别](#%E5%8C%BA%E5%88%AB) +- [三 生产者和消费者](#%E4%B8%89-%E7%94%9F%E4%BA%A7%E8%80%85%E5%92%8C%E6%B6%88%E8%B4%B9%E8%80%85) + - [synchroinzed](#synchroinzed) + - [lock](#lock) + - [按照线程顺序执行](#%E6%8C%89%E7%85%A7%E7%BA%BF%E7%A8%8B%E9%A1%BA%E5%BA%8F%E6%89%A7%E8%A1%8C) + + + + + +狂神JUC视频教程:https://www.bilibili.com/video/BV1B7411L7tE + + + +## 一 简介 + + + +### 什么是JUC + +JUC是java.util.concurrent 的简写,在并发编程中使用的工具类。 + +在jdk官方手册中可以看到juc相关的jar包有三个。 + +用中文概括一下,JUC的意思就是java并发编程工具包 + +实现多线程有三种方式:Thread、Runnable、Callable,其中Callable就位于concurrent包下 + +### 进程和线程 + +> 进程 / 线程是什么? + +进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。 + +线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源, 故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。 + +白话: + +进程:就是操作系统中运行的一个程序,QQ.exe,music.exe,word.exe ,这就是多个进程 + +线程:每个进程中都存在一个或者多个线程,比如用word写文章时,就会有一个线程默默帮你定时自动保存。 + +> 并发 / 并行是什么? + +做并发编程之前,必须首先理解什么是并发,什么是并行。 + +并发和并行是两个非常容易混淆的概念。它们都可以表示两个或多个任务一起执行,但是偏重点有点不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。然而并行的偏重点在于”同时执行”。 + +严格意义上来说,并行的多个任务是真实的同时执行,而对于并发来说,这个过程只是交替的,一会运行任务一,一会儿又运行任务二,系统会不停地在两者间切换。但对于外部观察者来说,即使多个任务是串行并发的,也会造成是多个任务并行执行的错觉。 + +实际上,如果系统内只有一个CPU,而现在而使用多线程或者多线程任务,那么真实环境中这些任务不可能真实并行的,毕竟一个CPU一次只能执行一条指令,这种情况下多线程或者多线程任务就是并发的,而不是并行,操作系统会不停的切换任务。真正的并发也只能够出现在拥有多个CPU的系统中(多核CPU)。 + +**并发的动机**:在计算能力恒定的情况下处理更多的任务, 就像我们的大脑, 计算能力相对恒定, 要在一天中处理更多的问题, 我们就必须具备多任务的能力. 现实工作中有很多事情可能会中断你的当前任务, 处理这种多任务的能力就是你的并发能力。 + +**并行的动机**:用更多的CPU核心更快的完成任务. 就像一个团队, 一个脑袋不够用了, 一个团队来一起处理 一个任务。 + +例子: +你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 +你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 (不一定是 +同时的) +你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。 +所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。 + + + +> 线程的状态 + +Java的线程有6种状态:可以分析源码: + +```java +public enum State { + //线程刚创建 + NEW, + + //在JVM中正在运行的线程 + RUNNABLE, + + //线程处于阻塞状态,等待监视锁,可以重新进行同步代码块中执行 + BLOCKED, + + //等待状态 + WAITING, + + //调用sleep() join() wait()方法可能导致线程处于等待状态 + TIMED_WAITING, + + //线程执行完毕,已经退出 + TERMINATED; +} +``` + +![image-20210513145542628](https://cdn.jsdelivr.net/gh/oddfar/static/img/JUC学习笔记.assets/image-20210513145542628.png) + +> wait / sleep 的区别 + +**1、来自不同的类** + +这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。 + +sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。 + +**2、有没有释放锁(释放资源)** + +最主要是sleep方法没有释放锁 + +而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 + + + +sleep是线程被调用时,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu。 + + + +sleep(100L)是占用cpu,线程休眠100毫秒,其他进程不能再占用cpu资源,wait(100L)是进入等待池中等待,交出cpu等系统资源供其他进程使用,在这100毫秒中,该线程可以被其他线程notify,但不同的是其他在等待池中的线程不被notify不会出来,但这个线程在等待100毫秒后会自动进入就绪队列等待系统分配资源,换句话说,sleep(100)在100毫秒后肯定会运行,但wait在100毫秒后还有等待os调用分配资源,所以wait100的停止运行时间是不确定的,但至少是100毫秒。 +就是说sleep有时间限制的就像闹钟一样到时候就叫了,而wait是无限期的除非用户主动notify。 + +**3、使用范围不同** + +wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 + +```java +synchronized(x){ + //或者wait() + x.notify() +} +``` + +**4、是否需要捕获异常** + +sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常 + +## 二 Lock锁 + +### synchronized锁 + +```java +public class SaleTicketTest1 { + /* + * 题目:三个售票员 卖出 30张票 + * 多线程编程的企业级套路: + * 1. 在高内聚低耦合的前提下, 线程 操作(对外暴露的调用方法) 资源类 + */ + + public static void main(String[] args) { + Ticket ticket = new Ticket(); + + new Thread(new Runnable() { + @Override + public void run() { + for (int i = 1; i <= 40; i++) { + ticket.saleTicket(); + } + } + }, "A").start(); + + new Thread(new Runnable() { + @Override + public void run() { + for (int i = 1; i <=40; i++) { + ticket.saleTicket(); + } + } + }, "B").start(); + + new Thread(new Runnable() { + @Override + public void run() { + for (int i = 1; i <= 40; i++) { + ticket.saleTicket(); + } + } + }, "C").start(); + + } + +} + +class Ticket { // 资源类 + private int number = 30; + + public synchronized void saleTicket() { + if (number > 0) { + System.out.println(Thread.currentThread().getName() + "卖出第 " + (number--) + "票,还剩下:" + number); + } + } +} +``` + +### Lock 锁 + +```java +public class SaleTicketTest2 { + public static void main(String[] args) { + Ticket2 ticket2 = new Ticket2(); + + new Thread(() -> { + for (int i = 1; i <= 40; i++) { + ticket2.saleTicket(); + } + }, "A").start(); + + new Thread(() -> { + for (int i = 1; i <= 40; i++) { + ticket2.saleTicket(); + } + }, "B").start(); + + new Thread(() -> { + for (int i = 1; i <= 40; i++) { + ticket2.saleTicket(); + } + }, "C").start(); + + } +} + +class Ticket2 { // 资源类 + private Lock lock = new ReentrantLock(); + + private int number = 30; + + public void saleTicket() { + lock.lock(); + + try { + if (number > 0) { + System.out.println(Thread.currentThread().getName() + "卖出第 " + (number--) + "票,还剩下:" + number); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + + } +} +``` + +### 区别 + +1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类; +2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁; +3. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放 +锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁; +4. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1 +阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以 +不用一直等待就结束了; +5. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可) +6. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。 + +## 三 生产者和消费者 + +### synchroinzed + +生产者和消费者 synchroinzed 版 + +```java +public class ProducerConsumer { + /** + * 题目:现在两个线程,可以操作初始值为0的一个变量 + * 实现一个线程对该变量 + 1,一个线程对该变量 -1 + * 实现交替10次 + *

+ * 诀窍: + * 1. 高内聚低耦合的前提下,线程操作资源类 + * 2. 判断 、干活、通知 + */ + + public static void main(String[] args) { + Data data = new Data(); + + //A线程增加 + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.increment(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "A").start(); + + //B线程减少 + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.decrement(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "B").start(); + } +} + +class Data { + private int number = 0; + + public synchronized void increment() throws InterruptedException { + // 判断该不该这个线程做 + if (number != 0) { + this.wait(); + } + // 干活 + number++; + System.out.println(Thread.currentThread().getName() + "\t" + number); + // 通知 + this.notifyAll(); + } + + public synchronized void decrement() throws InterruptedException { + // 判断该不该这个线程做 + if (number == 0) { + this.wait(); + } + // 干活 + number--; + System.out.println(Thread.currentThread().getName() + "\t" + number); + // 通知 + this.notifyAll(); + } + +} +``` + +问题升级:防止虚假唤醒,4个线程,两个加,两个减 + +【重点】if 和 while + +```java +public class ProducerConsumerPlus { + /** + * 题目:现在四个线程,可以操作初始值为0的一个变量 + * 实现两个线程对该变量 + 1,两个线程对该变量 -1 + * 实现交替10次 + * + * 诀窍: + * 1. 高内聚低耦合的前提下,线程操作资源类 + * 2. 判断 、干活、通知 + * 3. 多线程交互中,必须要防止多线程的虚假唤醒,即(判断不能用if,只能用while) + */ + + public static void main(String[] args) { + Data2 data = new Data2(); + + //A线程增加 + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.increment(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "A").start(); + + //B线程减少 + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.decrement(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "B").start(); + + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.increment(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "C").start(); + + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.decrement(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "D").start(); + } +} + +class Data2 { + private int number = 0; + + public synchronized void increment() throws InterruptedException { + // 判断该不该这个线程做 + while (number != 0) { + this.wait(); + } + // 干活 + number++; + System.out.println(Thread.currentThread().getName() + "\t" + number); + // 通知 + this.notifyAll(); + } + + public synchronized void decrement() throws InterruptedException { + // 判断该不该这个线程做 + while (number == 0) { + this.wait(); + } + // 干活 + number--; + System.out.println(Thread.currentThread().getName() + "\t" + number); + // 通知 + this.notifyAll(); + } + +} +``` + + + +### lock + + + +```java +public class ProducerConsumerPlus { + /** + * 题目:现在四个线程,可以操作初始值为0的一个变量 + * 实现两个线程对该变量 + 1,两个线程对该变量 -1 + * 实现交替10次 + *

+ * 诀窍: + * 1. 高内聚低耦合的前提下,线程操作资源类 + * 2. 判断 、干活、通知 + * 3. 多线程交互中,必须要防止多线程的虚假唤醒,即(判断不能用if,只能用while) + */ + + public static void main(String[] args) { + Data2 data = new Data2(); + + //A线程增加 + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.increment(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "A").start(); + + //B线程减少 + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.decrement(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "B").start(); + + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.increment(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "C").start(); + + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + try { + data.decrement(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "D").start(); + } +} + +class Data2 { + private int number = 0; + private Lock lock = new ReentrantLock(); + private Condition condition = lock.newCondition(); + + public void increment() throws InterruptedException { + + lock.lock(); + try { + // 判断该不该这个线程做 + while (number != 0) { + condition.await(); + } + // 干活 + number++; + System.out.println(Thread.currentThread().getName() + "\t" + number); + // 通知 + condition.signalAll(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public void decrement() throws InterruptedException { + lock.lock(); + try { + // 判断该不该这个线程做 + while (number == 0) { + condition.await(); + } + // 干活 + number--; + System.out.println(Thread.currentThread().getName() + "\t" + number); + // 通知 + condition.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + +} +``` + +以上写的程序并不会按照ABCD线程顺序,只会按照 “生产” “消费”顺序 + +### 按照线程顺序执行 + +精确通知顺序访问 + +```java +public class c { + /** + * 题目:多线程之间按顺序调用,实现 A->B->C + * 重点:标志位 + */ + + public static void main(String[] args) { + Resources resources = new Resources(); + + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + resources.a(); + } + + }, "A").start(); + + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + resources.b(); + } + + }, "B").start(); + + new Thread(() -> { + for (int i = 1; i <= 10; i++) { + resources.c(); + } + + }, "C").start(); + + } +} + +class Resources { + private int number = 1; // 1A 2B 3C + private Lock lock = new ReentrantLock(); + private Condition condition1 = lock.newCondition(); + private Condition condition2 = lock.newCondition(); + private Condition condition3 = lock.newCondition(); + + public void a() { + lock.lock(); + try { + // 判断 + while (number != 1) { + condition1.await(); + } + // 干活 + System.out.println(Thread.currentThread().getName()); + // 通知,指定的干活! + number = 2; + condition2.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public void b() { + lock.lock(); + try { + // 判断 + while (number != 2) { + condition2.await(); + } + // 干活 + System.out.println(Thread.currentThread().getName() ); + + // 通知,指定的干活! + number = 3; + condition3.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public void c() { + lock.lock(); + try { + // 判断 + while (number != 3) { + condition3.await(); + } + // 干活 + System.out.println(Thread.currentThread().getName()); + + // 通知,指定的干活! + number = 1; + condition1.signal(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } +} +``` \ No newline at end of file diff --git "a/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/03.JUC-\345\205\253\351\224\201.md" "b/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/03.JUC-\345\205\253\351\224\201.md" new file mode 100644 index 00000000..3deb624f --- /dev/null +++ "b/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/03.JUC-\345\205\253\351\224\201.md" @@ -0,0 +1,500 @@ +--- +title: JUC学习笔记-8锁的现象 +permalink: /java/se/thread/study-note/3 +date: 2021-05-16 11:01:20 +--- + +# 8锁的现象 + + + + + +- [问题一](#%E9%97%AE%E9%A2%98%E4%B8%80) +- [问题二](#%E9%97%AE%E9%A2%98%E4%BA%8C) +- [问题三](#%E9%97%AE%E9%A2%98%E4%B8%89) +- [问题四](#%E9%97%AE%E9%A2%98%E5%9B%9B) +- [问题五](#%E9%97%AE%E9%A2%98%E4%BA%94) +- [问题六](#%E9%97%AE%E9%A2%98%E5%85%AD) +- [问题七](#%E9%97%AE%E9%A2%98%E4%B8%83) +- [问题八](#%E9%97%AE%E9%A2%98%E5%85%AB) +- [小结](#%E5%B0%8F%E7%BB%93) + + + + + +## 问题一 + +1、标准访问,请问先打印邮件还是短信? + +```java +package com.oddfar.lock8; + +import java.util.concurrent.TimeUnit; + +public class A { + /** + * 多线程的8锁 + * 1、标准访问,请问先打印邮件还是短信? + */ + public static void main(String[] args) throws InterruptedException { + Phone phone = new Phone(); + new Thread(() -> { + try { + phone.sendEmail(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "A").start(); + + //休眠一秒 + //Thread.sleep(1000); + TimeUnit.SECONDS.sleep(1); + + new Thread(() -> { + try { + phone.sendSMS(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "B").start(); + + } +} + +class Phone { + public synchronized void sendEmail(){ + System.out.println("sendEmail"); + } + + public synchronized void sendSMS(){ + System.out.println("sendSMS"); + } +} + +``` + +答案:sendEmail + +结论:被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行。 + + + +## 问题二 + +2、邮件方法暂停4秒钟,请问先打印邮件还是短信? + +```java +package com.oddfar.lock8; + +import java.util.concurrent.TimeUnit; + +public class B { + /** + * 多线程的8锁 + * 2、邮件方法暂停4秒钟,请问先打印邮件还是短信? + */ + public static void main(String[] args) throws InterruptedException { + Phone2 phone = new Phone2(); + new Thread(() -> { + try { + phone.sendEmail(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "A").start(); + + TimeUnit.SECONDS.sleep(1); + + new Thread(() -> { + try { + phone.sendSMS(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "B").start(); + + } +} + +class Phone2 { + public synchronized void sendEmail() throws Exception { + TimeUnit.SECONDS.sleep(4); + System.out.println("sendEmail"); + } + + public synchronized void sendSMS(){ + System.out.println("sendSMS"); + } +} +``` + +答案:sendEmail + +结论:被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行,第二个方法只有在第一个方法执行完释放锁之后才能执行。 + +## 问题三 + +3、新增一个普通方法hello()不加锁,请问先打印邮件还是hello? + +```java +package com.oddfar.lock8; + +import java.util.concurrent.TimeUnit; + +/** + * @author zhiyuan + */ +public class C { + /** + * 多线程的8锁 + * 3、新增一个普通方法hello()不加锁,请问先打印邮件还是hello? + */ + public static void main(String[] args) throws InterruptedException { + Phone3 phone = new Phone3(); + new Thread(() -> { + try { + phone.sendEmail(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "A").start(); + + TimeUnit.SECONDS.sleep(1); + + new Thread(() -> { + try { + phone.hello(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "B").start(); + + } +} + +class Phone3 { + public synchronized void sendEmail() throws Exception { + TimeUnit.SECONDS.sleep(4); + System.out.println("sendEmail"); + } + + // 没有 synchronized,没有 static 就是普通方式 + public void hello() { + System.out.println("Hello"); + } +} +``` + +答案:Hello + +结论:如果一个方法没有被synchronized修饰,不是同步方法,不受锁的影响,所以不需要等待。 + +## 问题四 + +4、两个手机,一个手机发邮件,另一个发短信,请问先执行sendEmail 还是 sendSMS + +```java +package com.oddfar.lock8; + +import java.util.concurrent.TimeUnit; + +public class D { + /** + * 多线程的8锁 + * 4、两个手机,请问先执行sendEmail 还是 sendSMS + */ + public static void main(String[] args) throws InterruptedException { + Phone4 phone = new Phone4(); + Phone4 phone2 = new Phone4(); + + new Thread(() -> { + try { + phone.sendEmail(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "A").start(); + + TimeUnit.SECONDS.sleep(1); + + new Thread(() -> { + try { + phone2.sendSMS(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "B").start(); + + } +} + +class Phone4 { + public synchronized void sendEmail() throws Exception { + TimeUnit.SECONDS.sleep(4); + System.out.println("sendEmail"); + } + + public synchronized void sendSMS() { + System.out.println("sendSMS"); + } +} +``` + + + +答案:先执行“sendSMS” + +结论:被synchronized修饰的方法,锁的对象是方法的调用者。用了两个对象调用各自的方法,所以两个方法的调用者不是同一个,于是两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。 + +## 问题五 + +5、两个静态同步方法,同一部手机,请问先打印邮件还是短信? + +```java +package com.oddfar.lock8; + +import java.util.concurrent.TimeUnit; + +public class E { + /** + * 多线程的8锁 + * 5、两个静态同步方法,同一部手机,请问先打印邮件还是短信? + */ + public static void main(String[] args) throws InterruptedException { + Phone5 phone = new Phone5(); + + new Thread(() -> { + try { + phone.sendEmail(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "A").start(); + + TimeUnit.SECONDS.sleep(1); + + new Thread(() -> { + try { + phone.sendSMS(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "B").start(); + + } +} + +class Phone5 { + public static synchronized void sendEmail() throws Exception { + TimeUnit.SECONDS.sleep(4); + System.out.println("sendEmail"); + } + + public static synchronized void sendSMS() { + System.out.println("sendSMS"); + } +} +``` + + + +答案:先执行“sendEmail” + +结论:被synchronized和static修饰的方法,锁的对象是类的class模板对象,这个则全局唯一!两个方法都被static修饰了,所以两个方法用的是同一个锁,后调用的方法需要等待先调用的方法。 + +## 问题六 + +6、两个静态同步方法,2部手机,一个手机发邮件,另一个发短信,请问先打印邮件还是短信? + +```java +package com.oddfar.lock8; + +import java.util.concurrent.TimeUnit; + +/** + * @author zhiyuan + */ +public class F { + /** + * 多线程的8锁 + * 6、两个静态同步方法,2部手机,一个手机发邮件,另一个发短信,请问先打印邮件还是短信? + */ + public static void main(String[] args) throws InterruptedException { + Phone6 phone = new Phone6(); + + Phone6 phone2 = new Phone6(); + + new Thread(() -> { + try { + phone.sendEmail(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "A").start(); + + TimeUnit.SECONDS.sleep(1); + + new Thread(() -> { + try { + phone2.sendSMS(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "B").start(); + + } +} + +class Phone6 { + public static synchronized void sendEmail() throws Exception { + TimeUnit.SECONDS.sleep(4); + System.out.println("sendEmail"); + } + + public static synchronized void sendSMS() { + System.out.println("sendSMS"); + } +} +``` + +答案:先输出“sendEmail” + +结论:被synchronized和static修饰的方法,锁的对象就是Class模板对象,这个则全局唯一!所以说这里是同一个 + +## 问题七 + +7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信? + +```java +package com.oddfar.lock8; + +import java.util.concurrent.TimeUnit; + +public class G { + /** + * 多线程的8锁 + * 7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信? + */ + public static void main(String[] args) throws InterruptedException { + Phone7 phone = new Phone7(); + + new Thread(() -> { + try { + phone.sendEmail(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "A").start(); + + TimeUnit.SECONDS.sleep(1); + + new Thread(() -> { + try { + phone.sendSMS(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "B").start(); + + } +} + +class Phone7 { + public static synchronized void sendEmail() throws Exception { + TimeUnit.SECONDS.sleep(4); + System.out.println("sendEmail"); + } + + public synchronized void sendSMS() { + System.out.println("sendSMS"); + } +} +``` + +答案:先执行“sendSMS” + +结论:synchronized 锁的是这个调用的对象。被synchronized和static修饰的方法,锁的是这个类的Class模板 。这里是两个锁! + +## 问题八 + +8、一个普通同步方法,一个静态同步方法,2部手机,一个发邮件,一个发短信,请问哪个先执行? + +```java +package com.oddfar.lock8; + +import java.util.concurrent.TimeUnit; + +/** + * @author zhiyuan + */ +public class H { + /** + * 多线程的8锁 + * 8、一个普通同步方法,一个静态同步方法,2部手机,一个发邮件,一个发短信,请问先打印邮件还是短信? + */ + public static void main(String[] args) throws InterruptedException { + Phone8 phone = new Phone8(); + Phone8 phone2 = new Phone8(); + + new Thread(() -> { + try { + phone.sendEmail(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "A").start(); + + TimeUnit.SECONDS.sleep(1); + + new Thread(() -> { + try { + phone2.sendSMS(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "B").start(); + + } +} + +class Phone8 { + public static synchronized void sendEmail() throws Exception { + TimeUnit.SECONDS.sleep(4); + System.out.println("sendEmail"); + } + + public synchronized void sendSMS() { + System.out.println("sendSMS"); + } +} +``` + +答案:sendSMS + +结论:被synchronized和static修饰的方法,锁的对象是类的class对象。仅被synchronized修饰的方法,锁的对象是方法的调用者。即便是用同一个对象调用两个方法,锁的对象也不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。 + +## 小结 + +1、new this 调用的这个对象,是一个具体的对象! + +2、static class 唯一的一个模板! + +一个对象里面如果有多个synchronized方法,某个时刻内,只要一个线程去调用其中一个synchronized 方法了,其他的线程都要等待,换句话说,在某个时刻内,只能有唯一一个线程去访问这些 synchronized方法,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的 synchronized方法 + +加个普通方法后发现和同步锁无关,换成两个对象后,不是同一把锁,情况变化 + +都换成静态同步方法后,情况又变化了。所有的非静态的同步方法用的都是同一把锁(锁的class模板) + +**具体的表现为以下三种形式:** + +- 对于普通同步方法,锁的是当前实例对象 + +- 对于静态同步方法,锁的是当前的Class对象。 + +- 对于同步方法块,锁是synchronized括号里面的配置对象 + +当一个线程试图访问同步代码块时,他首先必须得到锁,退出或者是抛出异常时必须释放锁,也就是说 如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可以是别的实例对象非非静态同步方法因为跟该实例对象的非静态同步方法用 的是不同的锁,所以必须等待该实例对象已经获取锁的非静态同步方法释放锁就可以获取他们自己的 锁。 + +所有的静态同步方法用的也是同一把锁(类对象本身) ,这两把锁的是两个不同的对象,所以静态的同步方法与非静态的同步方法之间是不会有竞争条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要他们用一个的是同一个类的实例对象。 \ No newline at end of file diff --git "a/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/04.JUC\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/04.JUC\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 00000000..5d277684 --- /dev/null +++ "b/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/04.JUC\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,630 @@ +--- +title: JUC学习笔记(二) +permalink: /java/se/thread/study-note/4 +date: 2021-05-16 14:49:33 +--- + + + + + +- [六 多线程下集合类的不安全](#%E5%85%AD-%E5%A4%9A%E7%BA%BF%E7%A8%8B%E4%B8%8B%E9%9B%86%E5%90%88%E7%B1%BB%E7%9A%84%E4%B8%8D%E5%AE%89%E5%85%A8) + - [list](#list) + - [set](#set) + - [map](#map) +- [七 Callable](#%E4%B8%83-callable) + - [基础入门](#%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8) + - [多个线程调用](#%E5%A4%9A%E4%B8%AA%E7%BA%BF%E7%A8%8B%E8%B0%83%E7%94%A8) + - [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) +- [八 常用辅助类](#%E5%85%AB-%E5%B8%B8%E7%94%A8%E8%BE%85%E5%8A%A9%E7%B1%BB) + - [CountDownLatch](#countdownlatch) + - [CyclicBarrier](#cyclicbarrier) + - [Semaphore](#semaphore) +- [九 读写锁](#%E4%B9%9D-%E8%AF%BB%E5%86%99%E9%94%81) +- [十 阻塞队列](#%E5%8D%81-%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97) + - [阻塞队列简介](#%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E7%AE%80%E4%BB%8B) + - [阻塞队列的用处](#%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E7%9A%84%E7%94%A8%E5%A4%84) + - [接口架构图](#%E6%8E%A5%E5%8F%A3%E6%9E%B6%E6%9E%84%E5%9B%BE) + - [API的使用](#api%E7%9A%84%E4%BD%BF%E7%94%A8) + + + +## 六 多线程下集合类的不安全 + +### list + +多线程下 + +```java +public class ListTest { + public static void main(String[] args) { + List list = new ArrayList<>(); + // 对比3个线程 和 30个线程,看区别 + for (int i = 1; i <= 30; i++) { + new Thread(() -> { + list.add(UUID.randomUUID().toString().substring(0, 8)); + System.out.println(list); + }, String.valueOf(i)).start(); + } + } +} +``` + +运行报错:`java.util.ConcurrentModificationException` + +导致原因:add 方法没有加锁 + +解决方案: + +```java +/** + * 换一个集合类 + * 1、List list = new Vector<>(); JDK1.0 就存在了! + * 2、List list = Collections.synchronizedList(new ArrayList<>()); + * 3、List list = new CopyOnWriteArrayList<>(); + */ +public class ListTest { + public static void main(String[] args) { + + List list = new CopyOnWriteArrayList<>(); + + for (int i = 1; i <= 30; i++) { + new Thread(() -> { + list.add(UUID.randomUUID().toString().substring(0, 8)); + System.out.println(list); + }, String.valueOf(i)).start(); + } + } +} +``` + +**写入时复制(CopyOnWrite)思想** + +写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本 (private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。 + +**CopyOnWriteArrayList为什么并发安全且性能比Vector好** + +Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。 + +### set + + + +```java +/** + * 1、Set set = Collections.synchronizedSet(new HashSet<>()); + * 2、Set set = new CopyOnWriteArraySet(); + */ +public class SetTest { + public static void main(String[] args) { + + Set set = new CopyOnWriteArraySet(); + + for (int i = 1; i <= 30; i++) { + new Thread(() -> { + set.add(UUID.randomUUID().toString().substring(0, 8)); + System.out.println(set); + }, String.valueOf(i)).start(); + } + } +} +``` + +### map + +hashMap底层是数组+链表+红黑树 + +```java +Map map = new HashMap<>(); +// 等价于 +Map map = new HashMap<>(16,0.75); +// 工作中,常常会自己根据业务来写参数,提高效率 +``` + +map不安全测试: + +```java +public class MapSet { + public static void main(String[] args) { + Map map = new HashMap<>(); + + for (int i = 1; i <= 30; i++) { + new Thread(() -> { + map.put(Thread.currentThread().getName(), + UUID.randomUUID().toString().substring(0, 8)); + System.out.println(map); + }, String.valueOf(i)).start(); + } + } +} +``` + +解决: + +```java +Map map = new ConcurrentHashMap<>(); +``` + +## 七 Callable + +我们已经知道Java中常用的两种线程实现方式:分别是继承Thread类和实现Runnable接口。 + +![img](https://cdn.jsdelivr.net/gh/oddfar/static/img/JUC学习笔记.assets/95eef01f3a292df5cb9047105febf76635a87341.jpeg) + +从上图中,我们可以看到,第三种实现Callable接口的线程,而且还带有返回值的。我们来对比下实现Runnable和实现Callable接口的两种方式不同点: + +1:需要实现的方法名称不一样:一个run方法,一个call方法 + +2:返回值不同:一个void无返回值,一个带有返回值的。其中返回值的类型和泛型V是一致的。 + +3:异常:一个无需抛出异常,一个需要抛出异常。 + +### 基础入门 + +```java +public class CallableDemo { + public static void main(String[] args) throws Exception { + MyThread myThread = new MyThread(); + FutureTask futureTask = new FutureTask(myThread); // 适配类 + Thread t1 = new Thread(futureTask, "A"); // 调用执行 + t1.start(); + Integer result = (Integer) futureTask.get(); // 获取返回值 + System.out.println(result); + } +} + +class MyThread implements Callable { + @Override + public Integer call() throws Exception { + System.out.println("call 被调用"); + return 1024; + } +} +``` + +![image-20210516160612609](https://cdn.jsdelivr.net/gh/oddfar/static/img/JUC学习笔记.assets/image-20210516160612609.png) + +### 多个线程调用 + +```java +public class CallableDemo { + public static void main(String[] args) throws Exception { + MyThread myThread = new MyThread(); + FutureTask futureTask = new FutureTask(myThread); // 适配类 + + new Thread(futureTask, "A").start(); // 调用执行 + // 第二次调用执行,在同一个futureTask对象,不输出结果,可理解为“缓存” + new Thread(futureTask, "B").start(); + + //get 方法获得返回结果! 一般放在最后一行!否则可能会阻塞 + Integer result = (Integer) futureTask.get(); // 获取返回值 + System.out.println(result); + } +} + +class MyThread implements Callable { + @Override + public Integer call() throws Exception { + System.out.println(Thread.currentThread().getName() + "\tcall 被调用"); + TimeUnit.SECONDS.sleep(2); + return 1024; + } +} +``` + +### 参考资料 + +- https://baijiahao.baidu.com/s?id=1666820818587296272 + +## 八 常用辅助类 + +### CountDownLatch + +“倒计时锁存器” + +例如,执行完6个线程输出执行完毕 + +```java +public class CountDownLatchDemo { + public static void main(String[] args) throws InterruptedException { + // 计数器 + CountDownLatch countDownLatch = new CountDownLatch(6); + for (int i = 1; i <= 6; i++) { + new Thread(() -> { + System.out.println(Thread.currentThread().getName() + "\tStart"); + countDownLatch.countDown(); // 计数器-1 + }, String.valueOf(i)).start(); + } + //阻塞等待计数器归零 + countDownLatch.await(); + System.out.println(Thread.currentThread().getName() + "\tEnd"); + } + +} +``` + + + +CountDownLatch 主要有两个方法,当一个或多个线程调用 `await` 方法时,这些线程会阻塞 + +其他线程调用`CountDown()`方法会将计数器减1(调用CountDown方法的线程不会阻塞) + +当计数器变为0时,await 方法阻塞的线程会被唤醒,继续执行 + + + +### CyclicBarrier + +翻译:CyclicBarrier 篱栅 + +作用:和上面的减法相反,这里是加法,好比集齐7个龙珠召唤神龙,或者人到齐了再开会! + +```java +public class CyclicBarrierDemo { + public static void main(String[] args) { + // CyclicBarrier(int parties, Runnable barrierAction) + CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { + System.out.println("召唤神龙成功"); + }); + + for (int i = 1; i <= 7; i++) { + final int tempInt = i; + new Thread(() -> { + System.out.println(Thread.currentThread().getName() + + "收集了第" + tempInt + "颗龙珠"); + + try { + cyclicBarrier.await(); // 等待 + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (BrokenBarrierException e) { + e.printStackTrace(); + } + + }).start(); + } + } +} +``` + + + +### Semaphore + +翻译:Semaphore 信号量;信号灯;信号 + +举个“抢车位”的例子 + +```java +public class SemaphoreDemo { + public static void main(String[] args) { + // 模拟资源类,有3个空车位 + Semaphore semaphore = new Semaphore(3); + for (int i = 1; i <= 6; i++) { // 模拟6个车 + new Thread(() -> { + try { + semaphore.acquire(); // acquire 得到 + System.out.println(Thread.currentThread().getName() + " 抢到了车位"); + TimeUnit.SECONDS.sleep(3); // 停3秒钟 + System.out.println(Thread.currentThread().getName() + " 离开了车位"); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + semaphore.release(); // 释放这个位置 + } + }, String.valueOf(i)).start(); + } + + } +} +``` + +在信号量上我们定义两种操作: + +- acquire(获取) + + 当一个线程调用 acquire 操作时,他要么通过成功获取信号量(信号量-1) + + 要么一直等下去,直到有线程释放信号量,或超时 + +- release (释放) + + 会将信号量的值 + 1,然后唤醒等待的线程 + + + +信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。 + +## 九 读写锁 + +**ReadWriteLock** + +独占锁(写锁):指该锁一次只能被一个线程锁持有。对于ReentranrLock和 Synchronized 而言都是独占锁。 + +共享锁(读锁):该锁可被多个线程所持有。 + +对于ReentrantReadWriteLock其读锁时共享锁,写锁是独占锁,读锁的共享锁可保证并发读是非常高效的。 + +```java +public class ReadWriteLockDemo { + /** + * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。 + * 但是,如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写。 + * 1. 读-读 可以共存 + * 2. 读-写 不能共存 + * 3. 写-写 不能共存 + */ + public static void main(String[] args) { + MyCacheLock myCache = new MyCacheLock(); + // 写 + for (int i = 1; i <= 5; i++) { + final int tempInt = i; + new Thread(() -> { + myCache.put(tempInt + "", tempInt + ""); + }, String.valueOf(i)).start(); + } + + // 读 + for (int i = 1; i <= 5; i++) { + final int tempInt = i; + new Thread(() -> { + myCache.get(tempInt + ""); + }, String.valueOf(i)).start(); + } + } + +} + +// 测试发现问题: 写入的时候,还没写入完成,会存在其他的写入!造成问题 +class MyCache { + private volatile Map map = new HashMap<>(); + + public void put(String key, Object value) { + System.out.println(Thread.currentThread().getName() + " 写入" + key); + map.put(key, value); + System.out.println(Thread.currentThread().getName() + " 写入成功!"); + } + + public void get(String key) { + System.out.println(Thread.currentThread().getName() + " 读取" + key); + Object result = map.get(key); + System.out.println(Thread.currentThread().getName() + " 读取结果:" + result); + } +} + +// 加锁 +class MyCacheLock { + private volatile Map map = new HashMap<>(); + private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 读写锁 + + public void put(String key, Object value) { + // 写锁 + readWriteLock.writeLock().lock(); + try { + System.out.println(Thread.currentThread().getName() + " 写入" + key); + map.put(key, value); + System.out.println(Thread.currentThread().getName() + " 写入成功!"); + } catch (Exception e) { + e.printStackTrace(); + } finally { + //解锁 + readWriteLock.writeLock().unlock(); + } + } + + public void get(String key) { + // 读锁 + readWriteLock.readLock().lock(); + try { + System.out.println(Thread.currentThread().getName() + " 读取" + key); + Object result = map.get(key); + System.out.println(Thread.currentThread().getName() + " 读取结果:" + result); + } catch (Exception e) { + e.printStackTrace(); + } finally { + readWriteLock.readLock().unlock(); + } + } +} +``` + +## 十 阻塞队列 + +```java +Interface BlockingQueue +``` + +### 阻塞队列简介 + +阻塞:必须要阻塞、不得不阻塞 + +阻塞队列是一个队列,在数据结构中起的作用如下图: + +![image-20210517173258323](https://cdn.jsdelivr.net/gh/oddfar/static/img/JUC学习笔记.assets/image-20210517173258323.png) + +当队列是空的,从队列中**获取**元素的操作将会被阻塞。 + +当队列是满的,从队列中**添加**元素的操作将会被阻塞。 + +试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素。 + +试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增。 + +### 阻塞队列的用处 + +在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自 动被唤起。 + +为什么需要 BlockingQueue? + +好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue 都 给你一手包办了。 + +在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。 + +### 接口架构图 + +![image-20210517173544418](https://cdn.jsdelivr.net/gh/oddfar/static/img/JUC学习笔记.assets/image-20210517173544418.png) + +- ArrayBlockingQueue + + 由数组结构组成的有界阻塞队列。 + +- LinkedBlockingQueue + + 由链表结构组成的有界(默认值为:integer.MAX_VALUE)阻塞队列。 + +- PriorityBlockingQueue + + 支持优先级排序的无界阻塞队列 + +- DelayQueue + + 使用优先级队列实现的延迟无界阻塞队列。 + +- SynchronousQueue + + 不存储元素的阻塞队列,也即单个元素的队列。 + +- LinkedTransferQueue + + 由链表组成的无界阻塞队列 + +- LinkedBlockingDeque + + 由链表组成的双向阻塞队列。 + + + +### API的使用 + +| 方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 | +| ------------- | --------- | ---------- | -------- | ------------------ | +| 插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) | +| 移除方法 | remove() | poll() | take() | poll(time,unit) | +| 检查方法 | element() | peek() | 不可用 | 不可用 | + +解释: + +- 抛出异常 + +当阻塞队列满时,再往队列里add插入元素会抛出 `IllegalStateException: Queue full` + +当阻塞队列空时,再往队列里remove移除元素会抛 NoSuchElementException` + +- 返回特殊值 + +插入方法,成功返回true,失败则false + +移除方法,成功返回队列元素,队列里没有则返回null + +- 一直阻塞 + +当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据或响应中断退出 + +当阻塞队列空时,消费者线程从队列里take元素,队列会一直阻塞消费者线程直到队列可用 + +- 超时退出 + +当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出 + + + +**抛出异常** + +```java +package com.oddfar.bq; + +import java.util.concurrent.ArrayBlockingQueue; + +/** + * @author zhiyuan + */ +public class BlockingQueueDemo { + + public static void main(String[] args) { + // 队列大小 + ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); + System.out.println(blockingQueue.add("a")); + System.out.println(blockingQueue.add("b")); + System.out.println(blockingQueue.add("c")); + + //java.lang.IllegalStateException: Queue full +// System.out.println(blockingQueue.add("d")); + + System.out.println("首元素:" + blockingQueue.element()); // 检测队列队首元素! + // public E remove() 返回值E,就是移除的值 + System.out.println(blockingQueue.remove()); //a + System.out.println(blockingQueue.remove()); //b + System.out.println(blockingQueue.remove()); //c + // java.util.NoSuchElementException +// System.out.println(blockingQueue.remove()); + + } +} +``` + +**返回特殊值** + +```java +public class BlockingQueueDemo2 { + public static void main(String[] args) { + // 队列大小 + ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); + + System.out.println(blockingQueue.offer("a")); // true + System.out.println(blockingQueue.offer("b")); // true + System.out.println(blockingQueue.offer("c")); // true + //System.out.println(blockingQueue.offer("d")); // false + + System.out.println("首元素:" + blockingQueue.peek()); // 检测队列队首元素! + + // public E poll() + System.out.println(blockingQueue.poll()); // a + System.out.println(blockingQueue.poll()); // b + System.out.println(blockingQueue.poll()); // c + System.out.println(blockingQueue.poll()); // null + } +} +``` + +**一直阻塞** + +```java +public class BlockingQueueDemo3 { + public static void main(String[] args) throws InterruptedException { + // 队列大小 + ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); + + // 一直阻塞 + blockingQueue.put("a"); + blockingQueue.put("b"); + blockingQueue.put("c"); +// blockingQueue.put("d"); + System.out.println(blockingQueue.take()); // a + System.out.println(blockingQueue.take()); // b + System.out.println(blockingQueue.take()); // c + System.out.println(blockingQueue.take()); // 阻塞不停止等待 + } +} +``` + +**超时退出** + +```java +public class BlockingQueueDemo4 { + public static void main(String[] args) throws InterruptedException { + // 队列大小 + ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); + + // 一直阻塞 + blockingQueue.put("a"); + blockingQueue.put("b"); + blockingQueue.put("c"); + blockingQueue.offer("d",2L, TimeUnit.SECONDS); // 等待2秒超时退出 + + System.out.println(blockingQueue.take()); // a + System.out.println(blockingQueue.take()); // b + System.out.println(blockingQueue.take()); // c + System.out.println(blockingQueue.take()); // 阻塞不停止等待 + } +} +``` \ No newline at end of file diff --git "a/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 00000000..0aa6c156 --- /dev/null +++ "b/docs/01.Java/07.Java-\345\244\232\347\272\277\347\250\213/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260/05.JUC\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,18 @@ +--- +title: JUC学习笔记(三) +permalink: /java/se/thread/study-note/5 +date: 2021-05-17 18:01:25 +--- + + + + + +- [十 线程池](#%E5%8D%81-%E7%BA%BF%E7%A8%8B%E6%B1%A0) + + + +## 十 线程池 + +待补充 + diff --git "a/docs/01.Java/20.JavaWeb/01.\345\237\272\346\234\254\346\246\202\345\277\265.md" "b/docs/01.Java/20.JavaWeb/01.\345\237\272\346\234\254\346\246\202\345\277\265.md" new file mode 100644 index 00000000..a8da5cfb --- /dev/null +++ "b/docs/01.Java/20.JavaWeb/01.\345\237\272\346\234\254\346\246\202\345\277\265.md" @@ -0,0 +1,170 @@ +--- +title: 基本概念 +permalink: /javaweb/basic-concepts +categories: + - java + - java-web +date: 2021-05-15 18:09:11 +--- + +JavaWeb笔记转载于狂神笔记,稍修改了点内容 + +## 1、基本概念 + +### 1.1、前言 + +web开发: + +- web,网页的意思 , www.baidu.com +- 静态web + - html,css + - 提供给所有人看的数据始终不会发生变化! +- 动态web + - 淘宝,几乎是所有的网站; + - 提供给所有人看的数据始终会发生变化,每个人在不同的时间,不同的地点看到的信息各不相同! + - 技术栈:Servlet/JSP,ASP,PHP + +在Java中,动态web资源开发的技术统称为JavaWeb; + +### 1.2、web应用程序 + +web应用程序:可以提供浏览器访问的程序; + +- a.html、b.html......多个web资源,这些web资源可以被外界访问,对外界提供服务; +- 你们能访问到的任何一个页面或者资源,都存在于这个世界的某一个角落的计算机上。 +- URL +- 这个统一的web资源会被放在同一个文件夹下,web应用程序-->Tomcat:服务器 +- 一个web应用由多部分组成 (静态web,动态web) + - html,css,js + - jsp,servlet + - Java程序 + - jar包 + - 配置文件 (Properties) + +web应用程序编写完毕后,若想提供给外界访问:需要一个服务器来统一管理; + +### 1.3、静态web + +- *.htm, *.html,这些都是网页的后缀,如果服务器上一直存在这些东西,我们就可以直接进行读取。通络; + +![1567822802516](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567822802516.png) + +- 静态web存在的缺点 + - Web页面无法动态更新,所有用户看到都是同一个页面 + - 轮播图,点击特效:伪动态 + - JavaScript [实际开发中,它用的最多] + - VBScript + - 它无法和数据库交互(数据无法持久化,用户无法交互) + + + +### 1.4、动态web + +页面会动态展示: “Web的页面展示的效果因人而异”; + +![1567823191289](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567823191289.png) + +缺点: + +- 加入服务器的动态web资源出现了错误,我们需要重新编写我们的**后台程序**,重新发布; + - 停机维护 + +优点: + +- Web页面可以动态更新,所有用户看到都不是同一个页面 +- 它可以与数据库交互 (数据持久化:注册,商品信息,用户信息........) + +![1567823350584](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567823350584.png) + + +## 2、web服务器 + +### 2.1、技术讲解 + +**ASP:** + +- 微软:国内最早流行的就是ASP; + +- 在HTML中嵌入了VB的脚本, ASP + COM; + +- 在ASP开发中,基本一个页面都有几千行的业务代码,页面极其换乱 + +- 维护成本高! + +- C# + +- IIS + + ```html +

+

+

+

+

+

+ <% + System.out.println("hello") + %> +

+

+

+

+ ``` + + + +**php:** + +- PHP开发速度很快,功能很强大,跨平台,代码很简单 (70% , WP) +- 无法承载大访问量的情况(局限性) + + + +**JSP/Servlet : ** + +B/S:浏览和服务器 + +C/S: 客户端和服务器 + +- sun公司主推的B/S架构 +- 基于Java语言的 (所有的大公司,或者一些开源的组件,都是用Java写的) +- 可以承载三高问题带来的影响; +- 语法像ASP , ASP-->JSP , 加强市场强度; + + + +..... + + + +### 2.2、web服务器 + +服务器是一种被动的操作,用来处理用户的一些请求和给用户一些响应信息; + + + +**IIS** + +微软的; ASP...,Windows中自带的 + +**Tomcat** + +![1567824446428](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567824446428.png) + +面向百度编程; + +Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,最新的Servlet 和JSP 规范总是能在Tomcat 中得到体现,因为Tomcat 技术先进、性能稳定,而且**免费**,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。 + +Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用[服务器](https://baike.baidu.com/item/服务器),在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。对于一个Java初学web的人来说,它是最佳的选择 + +Tomcat 实际上运行JSP 页面和Servlet。Tomcat最新版本为**9.0。** + +.... + +**工作3-5年之后,可以尝试手写Tomcat服务器;** + +下载tomcat: + +1. 安装 or 解压 +2. 了解配置文件及目录结构 +3. 这个东西的作用 \ No newline at end of file diff --git a/docs/01.Java/20.JavaWeb/02.Tomcat.md b/docs/01.Java/20.JavaWeb/02.Tomcat.md new file mode 100644 index 00000000..5ab2b1eb --- /dev/null +++ b/docs/01.Java/20.JavaWeb/02.Tomcat.md @@ -0,0 +1,124 @@ +--- +title: Tomcat +permalink: /javaweb/tomcat +categories: + - java + - java-web +date: 2021-05-09 12:09:00 +--- + +# 3、Tomcat + +## 3.1、 安装tomcat + +tomcat官网:http://tomcat.apache.org/ + +![1567825600842](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567825600842.png) + +![1567825627138](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567825627138.png) + + + +## 3.2、Tomcat启动和配置 + +文件夹作用: + +![1567825763180](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567825763180.png) + +**启动。关闭Tomcat** + +![1567825840657](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567825840657.png) + +访问测试:http://localhost:8080/ + +可能遇到的问题: + +1. Java环境变量没有配置 +2. 闪退问题:需要配置兼容性 +3. 乱码问题:配置文件中设置 + +## 3.3、配置 + +![1567825967256](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567825967256.png) + +可以配置启动的端口号 + +- tomcat的默认端口号为:8080 +- mysql:3306 +- http:80 +- https:443 + +```xml + +``` +可以配置主机的名称 + +- 默认的主机名为:localhost->127.0.0.1 +- 默认网站应用存放的位置为:webapps + +```xml + +``` +### 高难度面试题: + +请你谈谈网站是如何进行访问的! + +1. 输入一个域名;回车 + +2. 检查本机的 C:\Windows\System32\drivers\etc\hosts配置文件下有没有这个域名映射; + + 1. 有:直接返回对应的ip地址,这个地址中,有我们需要访问的web程序,可以直接访问 + + ```java + 127.0.0.1 www.qinjiang.com + ``` + + 2. 没有:去DNS服务器找,找到的话就返回,找不到就返回找不到; + + ![1567827057913](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567827057913.png) + +4. 可以配置一下环境变量(可选性) + +## 3.4、发布一个web网站 + +不会就先模仿 + +- 将自己写的网站,放到服务器(Tomcat)中指定的web应用的文件夹(webapps)下,就可以访问了 + +网站应该有的结构 + +```java +--webapps :Tomcat服务器的web目录 + -ROOT + -kuangstudy :网站的目录名 + - WEB-INF + -classes : java程序 + -lib:web应用所依赖的jar包 + -web.xml :网站配置文件 + - index.html 默认的首页 + - static + -css + -style.css + -js + -img + -..... +``` + + + +HTTP协议 : 面试 + +Maven:构建工具 + +- Maven安装包 + +Servlet 入门 + +- HelloWorld! +- Servlet配置 +- 原理 + + diff --git a/docs/01.Java/20.JavaWeb/03.Http.md b/docs/01.Java/20.JavaWeb/03.Http.md new file mode 100644 index 00000000..6028794b --- /dev/null +++ b/docs/01.Java/20.JavaWeb/03.Http.md @@ -0,0 +1,124 @@ +--- +title: Http +permalink: /javaweb/http +date: 2021-05-06 14:38:43 +categories: + - java + - java-web +--- + + +# 4、Http + +## 4.1、什么是HTTP + +HTTP(超文本传输协议)是一个简单的请求-响应协议,它通常运行在TCP之上。 + +- 文本:html,字符串,~ …. +- 超文本:图片,音乐,视频,定位,地图……. +- 80 + +Https:安全的 + +- 443 + +## 4.2、两个时代 + +- http1.0 + + - HTTP/1.0:客户端可以与web服务器连接后,只能获得一个web资源,断开连接 + +- http2.0 + + - HTTP/1.1:客户端可以与web服务器连接后,可以获得多个web资源。‘ + + + +## 4.3、Http请求 + +- 客户端---发请求(Request)---服务器 + +百度: + +```java +Request URL:https://www.baidu.com/ 请求地址 +Request Method:GET get方法/post方法 +Status Code:200 OK 状态码:200 +Remote(远程) Address:14.215.177.39:443 +``` + +```java +Accept:text/html +Accept-Encoding:gzip, deflate, br +Accept-Language:zh-CN,zh;q=0.9 语言 +Cache-Control:max-age=0 +Connection:keep-alive +``` + +### 1、请求行 + +- 请求行中的请求方式:GET +- 请求方式:**Get,Post**,HEAD,DELETE,PUT,TRACT… + - get:请求能够携带的参数比较少,大小有限制,会在浏览器的URL地址栏显示数据内容,不安全,但高效 + - post:请求能够携带的参数没有限制,大小没有限制,不会在浏览器的URL地址栏显示数据内容,安全,但不高效。 + +### 2、消息头 + +```java +Accept:告诉浏览器,它所支持的数据类型 +Accept-Encoding:支持哪种编码格式 GBK UTF-8 GB2312 ISO8859-1 +Accept-Language:告诉浏览器,它的语言环境 +Cache-Control:缓存控制 +Connection:告诉浏览器,请求完成是断开还是保持连接 +HOST:主机..../. +``` + +## 4.4、Http响应 + +- 服务器---响应-----客户端 + +百度: + +```java +Cache-Control:private 缓存控制 +Connection:Keep-Alive 连接 +Content-Encoding:gzip 编码 +Content-Type:text/html 类型 +``` + +### 1.响应体 + +```java +Accept:告诉浏览器,它所支持的数据类型 +Accept-Encoding:支持哪种编码格式 GBK UTF-8 GB2312 ISO8859-1 +Accept-Language:告诉浏览器,它的语言环境 +Cache-Control:缓存控制 +Connection:告诉浏览器,请求完成是断开还是保持连接 +HOST:主机..../. +Refresh:告诉客户端,多久刷新一次; +Location:让网页重新定位; +``` + +### 2、响应状态码 + +200:请求响应成功 200 + +3xx:请求重定向 + +- 重定向:你重新到我给你新位置去; + +4xx:找不到资源 404 + +- 资源不存在; + +5xx:服务器代码错误 500 502:网关错误 + + + +**常见面试题:** + +当你的浏览器中地址栏输入地址并回车的一瞬间到页面能够展示回来,经历了什么? + + + + diff --git a/docs/01.Java/20.JavaWeb/04.Maven.md b/docs/01.Java/20.JavaWeb/04.Maven.md new file mode 100644 index 00000000..e548f806 --- /dev/null +++ b/docs/01.Java/20.JavaWeb/04.Maven.md @@ -0,0 +1,343 @@ +--- +title: Maven +permalink: /javaweb/maven +date: 2021-05-06 14:38:43 +categories: + - java + - java-web +--- + +# 5、Maven + +**我为什么要学习这个技术?** + +1. 在Javaweb开发中,需要使用大量的jar包,我们手动去导入; + +2. 如何能够让一个东西自动帮我导入和配置这个jar包。 + + 由此,Maven诞生了! + + + +## 5.1 Maven项目架构管理工具 + +我们目前用来就是方便导入jar包的! + +Maven的核心思想:**约定大于配置** + +- 有约束,不要去违反。 + +Maven会规定好你该如何去编写我们的Java代码,必须要按照这个规范来; + +## 5.2 下载安装Maven + +官网;https://maven.apache.org/ + +![1567842350606](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567842350606.png) + +下载完成后,解压即可; + +小狂神友情建议:电脑上的所有环境都放在一个文件夹下,方便管理; + + + +## 5.3 配置环境变量 + +在我们的系统环境变量中 + +配置如下配置: + +- M2_HOME maven目录下的bin目录 +- MAVEN_HOME maven的目录 +- 在系统的path中配置 %MAVEN_HOME%\bin + +![1567842882993](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567842882993.png) + +测试Maven是否安装成功,保证必须配置完毕! + +## 5.4 阿里云镜像 + +![1567844609399](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567844609399.png) + +- 镜像:mirrors + - 作用:加速我们的下载 +- 国内建议使用阿里云的镜像 + +```xml + + nexus-aliyun + *,!jeecg,!jeecg-snapshots + Nexus aliyun + http://maven.aliyun.com/nexus/content/groups/public + +``` + +## 5.5 本地仓库 + +在本地的仓库,远程仓库; + +**建立一个本地仓库:**localRepository + +```xml +D:\Environment\apache-maven-3.6.2\maven-repo +``` + +## 5.6、在IDEA中使用Maven + +1. 启动IDEA + +2. 创建一个MavenWeb项目 + + ![1567844785602](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567844785602.png) + + ![1567844841172](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567844841172.png) + + ![1567844917185](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567844917185.png) + + ![1567844956177](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567844956177.png) + + ![1567845029864](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845029864.png) + +3. 等待项目初始化完毕 + + ![1567845105970](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845105970.png) + + ![1567845137978](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845137978.png) + +4. 观察maven仓库中多了什么东西? + +5. IDEA中的Maven设置 + + 注意:IDEA项目创建成功后,看一眼Maven的配置 + + ![1567845341956](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845341956.png) + + ![1567845413672](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845413672.png) + +6. 到这里,Maven在IDEA中的配置和使用就OK了! + +## 5.7、创建一个普通的Maven项目 + +![1567845557744](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845557744.png) + +![1567845717377](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845717377.png) + +这个只有在Web应用下才会有! + +![1567845782034](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845782034.png) + +## 5.8 标记文件夹功能 + +![1567845910728](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845910728.png) + +![1567845957139](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567845957139.png) + +![1567846034906](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846034906.png) + +![1567846073511](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846073511.png) + +## 5.9 在 IDEA中配置Tomcat + +![1567846140348](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846140348.png) + +![1567846179573](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846179573.png) + +![1567846234175](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846234175.png) + +![1567846369751](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846369751.png) + +解决警告问题 + +必须要的配置:**为什么会有这个问题:我们访问一个网站,需要指定一个文件夹名字;** + +![1567846421963](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846421963.png) + +![1567846546465](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846546465.png) + +![1567846559111](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846559111.png) + +![1567846640372](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846640372.png) + +## 5.10 pom文件 + +pom.xml 是Maven的核心配置文件 + +![1567846784849](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567846784849.png) + +```xml + + + + + 4.0.0 + + + com.kuang + javaweb-01-maven + 1.0-SNAPSHOT + + war + + + + + + UTF-8 + + 1.8 + 1.8 + + + + + + + junit + junit + 4.11 + + + + + + javaweb-01-maven + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-war-plugin + 3.2.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + + + +``` + +![1567847410771](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567847410771.png) + + + +maven由于他的约定大于配置,我们之后可以能遇到我们写的配置文件,无法被导出或者生效的问题,解决方案: + +```xml + + + + + src/main/resources + + **/*.properties + **/*.xml + + true + + + src/main/java + + **/*.properties + **/*.xml + + true + + + +``` + + + +## 5.12 IDEA操作 + +![1567847630808](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567847630808.png) + + + +![1567847662429](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567847662429.png) + + + +## 5.13 解决遇到的问题 + +1. Maven 3.6.2 + + 解决方法:降级为3.6.1 + + ![1567904721301](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567904721301.png) + +2. Tomcat闪退 + + + +3. IDEA中每次都要重复配置Maven + 在IDEA中的全局默认配置中去配置 + + ![1567905247201](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567905247201.png) + + ![1567905291002](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567905291002.png) + +4. Maven项目中Tomcat无法配置 + +5. maven默认web项目中的web.xml版本问题 + + ![1567905537026](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567905537026.png) + +6. 替换为webapp4.0版本和tomcat一致 + + ```xml + + + + + + + ``` + + + +7. Maven仓库的使用 + + 地址:https://mvnrepository.com/ + + ![1567905870750](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567905870750.png) + + ![1567905982979](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567905982979.png) + + ![1567906017448](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567906017448.png) + + ![1567906039469](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567906039469.png) + + diff --git "a/docs/01.Java/20.JavaWeb/20.\346\200\273\350\247\210.md" "b/docs/01.Java/20.JavaWeb/20.\346\200\273\350\247\210.md" new file mode 100644 index 00000000..e25598c3 --- /dev/null +++ "b/docs/01.Java/20.JavaWeb/20.\346\200\273\350\247\210.md" @@ -0,0 +1,2054 @@ +--- +title: java-web总览 +permalink: /javaweb/overview +author: + name: 致远 + link: https://oddfar.com +categories: + - java + - java-web +categoryText: java +date: 2021-05-07 18:09:11 +--- + + +# JavaWeb + + +## 6、Servlet + +### 6.1、Servlet简介 + +- Servlet就是sun公司开发动态web的一门技术 +- Sun在这些API中提供一个接口叫做:Servlet,如果你想开发一个Servlet程序,只需要完成两个小步骤: + - 编写一个类,实现Servlet接口 + - 把开发好的Java类部署到web服务器中。 + +**把实现了Servlet接口的Java程序叫做,Servlet** + +### 6.2、HelloServlet + +Serlvet接口Sun公司有两个默认的实现类:HttpServlet,GenericServlet + + + +1. 构建一个普通的Maven项目,删掉里面的src目录,以后我们的学习就在这个项目里面建立Moudel;这个空的工程就是Maven主工程; + +2. 关于Maven父子工程的理解: + + 父项目中会有 + + ```xml + + servlet-01 + + ``` + + 子项目会有 + + ```xml + + javaweb-02-servlet + com.kuang + 1.0-SNAPSHOT + + ``` + + 父项目中的java子项目可以直接使用 + + ```java + son extends father + ``` + +3. Maven环境优化 + + 1. 修改web.xml为最新的 + 2. 将maven的结构搭建完整 + +4. 编写一个Servlet程序 + + ![1567911804700](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567911804700.png) + + 1. 编写一个普通类 + + 2. 实现Servlet接口,这里我们直接继承HttpServlet + + ```java + public class HelloServlet extends HttpServlet { + + //由于get或者post只是请求实现的不同的方式,可以相互调用,业务逻辑都一样; + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + //ServletOutputStream outputStream = resp.getOutputStream(); + PrintWriter writer = resp.getWriter(); //响应流 + writer.print("Hello,Serlvet"); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } + } + + ``` + +5. 编写Servlet的映射 + + 为什么需要映射:我们写的是JAVA程序,但是要通过浏览器访问,而浏览器需要连接web服务器,所以我们需要再web服务中注册我们写的Servlet,还需给他一个浏览器能够访问的路径; + + ```xml + + + + hello + com.kuang.servlet.HelloServlet + + + + hello + /hello + + + ``` + + + +6. 配置Tomcat + + 注意:配置项目发布的路径就可以了 + +7. 启动测试,OK! + +### 6.3、Servlet原理 + +Servlet是由Web服务器调用,web服务器在收到浏览器请求之后,会: + +![1567913793252](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567913793252.png) + +### 6.4、Mapping问题 + +1. 一个Servlet可以指定一个映射路径 + + ```xml + + hello + /hello + + ``` + +2. 一个Servlet可以指定多个映射路径 + + ```xml + + hello + /hello + + + hello + /hello2 + + + hello + /hello3 + + + hello + /hello4 + + + hello + /hello5 + + + ``` + +3. 一个Servlet可以指定通用映射路径 + + ```xml + + hello + /hello/* + + ``` + +4. 默认请求路径 + + ```xml + + + hello + /* + + ``` + +5. 指定一些后缀或者前缀等等…. + + ```xml + + + + hello + *.qinjiang + + ``` + +6. 优先级问题 + 指定了固有的映射路径优先级最高,如果找不到就会走默认的处理请求; + + ```xml + + + error + com.kuang.servlet.ErrorServlet + + + error + /* + + + ``` + + + +### 6.5、ServletContext + +web容器在启动的时候,它会为每个web程序都创建一个对应的ServletContext对象,它代表了当前的web应用; + +#### 1、共享数据 + +我在这个Servlet中保存的数据,可以在另外一个servlet中拿到; + +```java +public class HelloServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + //this.getInitParameter() 初始化参数 + //this.getServletConfig() Servlet配置 + //this.getServletContext() Servlet上下文 + ServletContext context = this.getServletContext(); + + String username = "秦疆"; //数据 + context.setAttribute("username",username); //将一个数据保存在了ServletContext中,名字为:username 。值 username + + } + +} + +``` + +```java +public class GetServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ServletContext context = this.getServletContext(); + String username = (String) context.getAttribute("username"); + + resp.setContentType("text/html"); + resp.setCharacterEncoding("utf-8"); + resp.getWriter().print("名字"+username); + + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } +} + +``` + +```XML + + hello + com.kuang.servlet.HelloServlet + + + hello + /hello + + + + + getc + com.kuang.servlet.GetServlet + + + getc + /getc + +``` + +测试访问结果; + + + +#### 2、获取初始化参数 + +```xml + + + url + jdbc:mysql://localhost:3306/mybatis + +``` + +```java +protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ServletContext context = this.getServletContext(); + String url = context.getInitParameter("url"); + resp.getWriter().print(url); +} +``` + +#### 3、请求转发 + +```java +@Override +protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ServletContext context = this.getServletContext(); + System.out.println("进入了ServletDemo04"); + //RequestDispatcher requestDispatcher = context.getRequestDispatcher("/gp"); //转发的请求路径 + //requestDispatcher.forward(req,resp); //调用forward实现请求转发; + context.getRequestDispatcher("/gp").forward(req,resp); +} +``` + +![1567924457532](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567924457532.png) + +#### 4、读取资源文件 + +Properties + +- 在java目录下新建properties +- 在resources目录下新建properties + +发现:都被打包到了同一个路径下:classes,我们俗称这个路径为classpath: + +思路:需要一个文件流; + +```properties +username=root12312 +password=zxczxczxc +``` + +```java +public class ServletDemo05 extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + InputStream is = this.getServletContext().getResourceAsStream("/WEB-INF/classes/com/kuang/servlet/aa.properties"); + + Properties prop = new Properties(); + prop.load(is); + String user = prop.getProperty("username"); + String pwd = prop.getProperty("password"); + + resp.getWriter().print(user+":"+pwd); + + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } +} + +``` + +访问测试即可ok; + +### 6.6、HttpServletResponse + +web服务器接收到客户端的http请求,针对这个请求,分别创建一个代表请求的HttpServletRequest对象,代表响应的一个HttpServletResponse; + +- 如果要获取客户端请求过来的参数:找HttpServletRequest +- 如果要给客户端响应一些信息:找HttpServletResponse + +#### 1、简单分类 + +负责向浏览器发送数据的方法 + +```java +ServletOutputStream getOutputStream() throws IOException; +PrintWriter getWriter() throws IOException; +``` + +负责向浏览器发送响应头的方法 + +```java + void setCharacterEncoding(String var1); + + void setContentLength(int var1); + + void setContentLengthLong(long var1); + + void setContentType(String var1); + + void setDateHeader(String var1, long var2); + + void addDateHeader(String var1, long var2); + + void setHeader(String var1, String var2); + + void addHeader(String var1, String var2); + + void setIntHeader(String var1, int var2); + + void addIntHeader(String var1, int var2); +``` + +响应的状态码 + +```java + int SC_CONTINUE = 100; + int SC_SWITCHING_PROTOCOLS = 101; + int SC_OK = 200; + int SC_CREATED = 201; + int SC_ACCEPTED = 202; + int SC_NON_AUTHORITATIVE_INFORMATION = 203; + int SC_NO_CONTENT = 204; + int SC_RESET_CONTENT = 205; + int SC_PARTIAL_CONTENT = 206; + int SC_MULTIPLE_CHOICES = 300; + int SC_MOVED_PERMANENTLY = 301; + int SC_MOVED_TEMPORARILY = 302; + int SC_FOUND = 302; + int SC_SEE_OTHER = 303; + int SC_NOT_MODIFIED = 304; + int SC_USE_PROXY = 305; + int SC_TEMPORARY_REDIRECT = 307; + int SC_BAD_REQUEST = 400; + int SC_UNAUTHORIZED = 401; + int SC_PAYMENT_REQUIRED = 402; + int SC_FORBIDDEN = 403; + int SC_NOT_FOUND = 404; + int SC_METHOD_NOT_ALLOWED = 405; + int SC_NOT_ACCEPTABLE = 406; + int SC_PROXY_AUTHENTICATION_REQUIRED = 407; + int SC_REQUEST_TIMEOUT = 408; + int SC_CONFLICT = 409; + int SC_GONE = 410; + int SC_LENGTH_REQUIRED = 411; + int SC_PRECONDITION_FAILED = 412; + int SC_REQUEST_ENTITY_TOO_LARGE = 413; + int SC_REQUEST_URI_TOO_LONG = 414; + int SC_UNSUPPORTED_MEDIA_TYPE = 415; + int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + int SC_EXPECTATION_FAILED = 417; + int SC_INTERNAL_SERVER_ERROR = 500; + int SC_NOT_IMPLEMENTED = 501; + int SC_BAD_GATEWAY = 502; + int SC_SERVICE_UNAVAILABLE = 503; + int SC_GATEWAY_TIMEOUT = 504; + int SC_HTTP_VERSION_NOT_SUPPORTED = 505; +``` + +#### 2、下载文件 + +1. 向浏览器输出消息 (一直在讲,就不说了) +2. 下载文件 + 1. 要获取下载文件的路径 + 2. 下载的文件名是啥? + 3. 设置想办法让浏览器能够支持下载我们需要的东西 + 4. 获取下载文件的输入流 + 5. 创建缓冲区 + 6. 获取OutputStream对象 + 7. 将FileOutputStream流写入到buffer缓冲区 + 8. 使用OutputStream将缓冲区中的数据输出到客户端! + +```java +@Override +protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // 1. 要获取下载文件的路径 + String realPath = "F:\\班级管理\\西开【19525】\\2、代码\\JavaWeb\\javaweb-02-servlet\\response\\target\\classes\\秦疆.png"; + System.out.println("下载文件的路径:"+realPath); + // 2. 下载的文件名是啥? + String fileName = realPath.substring(realPath.lastIndexOf("\\") + 1); + // 3. 设置想办法让浏览器能够支持(Content-Disposition)下载我们需要的东西,中文文件名URLEncoder.encode编码,否则有可能乱码 + resp.setHeader("Content-Disposition","attachment;filename="+URLEncoder.encode(fileName,"UTF-8")); + // 4. 获取下载文件的输入流 + FileInputStream in = new FileInputStream(realPath); + // 5. 创建缓冲区 + int len = 0; + byte[] buffer = new byte[1024]; + // 6. 获取OutputStream对象 + ServletOutputStream out = resp.getOutputStream(); + // 7. 将FileOutputStream流写入到buffer缓冲区,使用OutputStream将缓冲区中的数据输出到客户端! + while ((len=in.read(buffer))>0){ + out.write(buffer,0,len); + } + + in.close(); + out.close(); +} +``` + +#### 3、验证码功能 + +验证怎么来的? + +- 前端实现 +- 后端实现,需要用到 Java 的图片类,生产一个图片 + +```java +package com.kuang.servlet; + +import javax.imageio.ImageIO; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Random; + +public class ImageServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + //如何让浏览器3秒自动刷新一次; + resp.setHeader("refresh","3"); + + //在内存中创建一个图片 + BufferedImage image = new BufferedImage(80,20,BufferedImage.TYPE_INT_RGB); + //得到图片 + Graphics2D g = (Graphics2D) image.getGraphics(); //笔 + //设置图片的背景颜色 + g.setColor(Color.white); + g.fillRect(0,0,80,20); + //给图片写数据 + g.setColor(Color.BLUE); + g.setFont(new Font(null,Font.BOLD,20)); + g.drawString(makeNum(),0,20); + + //告诉浏览器,这个请求用图片的方式打开 + resp.setContentType("image/jpeg"); + //网站存在缓存,不让浏览器缓存 + resp.setDateHeader("expires",-1); + resp.setHeader("Cache-Control","no-cache"); + resp.setHeader("Pragma","no-cache"); + + //把图片写给浏览器 + ImageIO.write(image,"jpg", resp.getOutputStream()); + + } + + //生成随机数 + private String makeNum(){ + Random random = new Random(); + String num = random.nextInt(9999999) + ""; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 7-num.length() ; i++) { + sb.append("0"); + } + num = sb.toString() + num; + return num; + } + + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } +} + +``` + +#### 4、实现重定向 + +![1567931587955](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567931587955.png) + +B一个web资源收到客户端A请求后,B他会通知A客户端去访问另外一个web资源C,这个过程叫重定向 + +常见场景: + +- 用户登录 + +```java +void sendRedirect(String var1) throws IOException; +``` + +测试: + +```java +@Override +protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + /* + resp.setHeader("Location","/r/img"); + resp.setStatus(302); + */ + resp.sendRedirect("/r/img");//重定向 +} +``` + +面试题:请你聊聊重定向和转发的区别? + +相同点 + +- 页面都会实现跳转 + +不同点 + +- 请求转发的时候,url不会产生变化 +- 重定向时候,url地址栏会发生变化; + +![1567932163430](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567932163430.png) + +#### 5、简单实现登录重定向 + +```jsp +<%--这里提交的路径,需要寻找到项目的路径--%> +<%--${pageContext.request.contextPath}代表当前的项目--%> + +
+ 用户名:
+ 密码:
+ +
+ +``` + +```JAVA + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + //处理请求 + String username = req.getParameter("username"); + String password = req.getParameter("password"); + + System.out.println(username+":"+password); + + //重定向时候一定要注意,路径问题,否则404; + resp.sendRedirect("/r/success.jsp"); + } + +``` + +```xml + + requset + com.kuang.servlet.RequestTest + + + requset + /login + +``` + +```jsp +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Title + + + +

Success

+ + + + +``` + +### 6.7、HttpServletRequest + +HttpServletRequest代表客户端的请求,用户通过Http协议访问服务器,HTTP请求中的所有信息会被封装到HttpServletRequest,通过这个HttpServletRequest的方法,获得客户端的所有信息; + +![1567933996830](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567933996830.png) + +![1567934023106](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567934023106.png) + +#### 获取参数,请求转发 + +![1567934110794](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1567934110794.png) + +```java +@Override +protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + req.setCharacterEncoding("utf-8"); + resp.setCharacterEncoding("utf-8"); + + String username = req.getParameter("username"); + String password = req.getParameter("password"); + String[] hobbys = req.getParameterValues("hobbys"); + System.out.println("============================="); + //后台接收中文乱码问题 + System.out.println(username); + System.out.println(password); + System.out.println(Arrays.toString(hobbys)); + System.out.println("============================="); + + + System.out.println(req.getContextPath()); + //通过请求转发 + //这里的 / 代表当前的web应用 + req.getRequestDispatcher("/success.jsp").forward(req,resp); + +} +``` + +**面试题:请你聊聊重定向和转发的区别?** + +相同点 + +- 页面都会实现跳转 + +不同点 + +- 请求转发的时候,url不会产生变化 307 +- 重定向时候,url地址栏会发生变化; 302 + + + +## 7、Cookie、Session + +### 7.1、会话 + +**会话**:用户打开一个浏览器,点击了很多超链接,访问多个web资源,关闭浏览器,这个过程可以称之为会话; + +**有状态会话**:一个同学来过教室,下次再来教室,我们会知道这个同学,曾经来过,称之为有状态会话; + +**你能怎么证明你是西开的学生?** + +你 西开 + +1. 发票 西开给你发票 +2. 学校登记 西开标记你来过了 + +**一个网站,怎么证明你来过?** + +客户端 服务端 + +1. 服务端给客户端一个 信件,客户端下次访问服务端带上信件就可以了; cookie +2. 服务器登记你来过了,下次你来的时候我来匹配你; seesion + + + +### 7.2、保存会话的两种技术 + +**cookie** + +- 客户端技术 (响应,请求) + +**session** + +- 服务器技术,利用这个技术,可以保存用户的会话信息? 我们可以把信息或者数据放在Session中! + + + +常见常见:网站登录之后,你下次不用再登录了,第二次访问直接就上去了! + +### 7.3、Cookie + +![1568344447291](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568344447291.png) + +1. 从请求中拿到cookie信息 +2. 服务器响应给客户端cookie + +```java +Cookie[] cookies = req.getCookies(); //获得Cookie +cookie.getName(); //获得cookie中的key +cookie.getValue(); //获得cookie中的vlaue +new Cookie("lastLoginTime", System.currentTimeMillis()+""); //新建一个cookie +cookie.setMaxAge(24*60*60); //设置cookie的有效期 +resp.addCookie(cookie); //响应给客户端一个cookie +``` + +**cookie:一般会保存在本地的 用户目录下 appdata;** + + + +一个网站cookie是否存在上限!**聊聊细节问题** + +- 一个Cookie只能保存一个信息; +- 一个web站点可以给浏览器发送多个cookie,最多存放20个cookie; +- Cookie大小有限制4kb; +- 300个cookie浏览器上限 + + + +**删除Cookie;** + +- 不设置有效期,关闭浏览器,自动失效; +- 设置有效期时间为 0 ; + + + +**编码解码:** + +```java +URLEncoder.encode("秦疆","utf-8") +URLDecoder.decode(cookie.getValue(),"UTF-8") +``` + + + +### 7.4、Session(重点) + +![1568344560794](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568344560794.png) + +什么是Session: + +- 服务器会给每一个用户(浏览器)创建一个Seesion对象; +- 一个Seesion独占一个浏览器,只要浏览器没有关闭,这个Session就存在; +- 用户登录之后,整个网站它都可以访问!--> 保存用户的信息;保存购物车的信息….. + +![1568342773861](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568342773861.png) + +Session和cookie的区别: + +- Cookie是把用户的数据写给用户的浏览器,浏览器保存 (可以保存多个) +- Session把用户的数据写到用户独占Session中,服务器端保存 (保存重要的信息,减少服务器资源的浪费) +- Session对象由服务创建; + + + +使用场景: + +- 保存一个登录用户的信息; +- 购物车信息; +- 在整个网站中经常会使用的数据,我们将它保存在Session中; + + + +使用Session: + +```java +package com.kuang.servlet; + +import com.kuang.pojo.Person; + +import javax.servlet.ServletException; +import javax.servlet.http.*; +import java.io.IOException; + +public class SessionDemo01 extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + //解决乱码问题 + req.setCharacterEncoding("UTF-8"); + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/html;charset=utf-8"); + + //得到Session + HttpSession session = req.getSession(); + //给Session中存东西 + session.setAttribute("name",new Person("秦疆",1)); + //获取Session的ID + String sessionId = session.getId(); + + //判断Session是不是新创建 + if (session.isNew()){ + resp.getWriter().write("session创建成功,ID:"+sessionId); + }else { + resp.getWriter().write("session以及在服务器中存在了,ID:"+sessionId); + } + + //Session创建的时候做了什么事情; +// Cookie cookie = new Cookie("JSESSIONID",sessionId); +// resp.addCookie(cookie); + + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } +} + +//得到Session +HttpSession session = req.getSession(); + +Person person = (Person) session.getAttribute("name"); + +System.out.println(person.toString()); + +HttpSession session = req.getSession(); +session.removeAttribute("name"); +//手动注销Session +session.invalidate(); +``` + + + +**会话自动过期:web.xml配置** + +```xml + + + + 15 + +``` + + + +![1568344679763](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568344679763.png) + + + +## 8、JSP + +### 8.1、什么是JSP + +Java Server Pages : Java服务器端页面,也和Servlet一样,用于动态Web技术! + +最大的特点: + +- 写JSP就像在写HTML +- 区别: + - HTML只给用户提供静态的数据 + - JSP页面中可以嵌入JAVA代码,为用户提供动态数据; + + + +### 8.2、JSP原理 + +思路:JSP到底怎么执行的! + +- 代码层面没有任何问题 + +- 服务器内部工作 + + tomcat中有一个work目录; + + IDEA中使用Tomcat的会在IDEA的tomcat中生产一个work目录 + + ![1568345873736](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568345873736.png) + + 我电脑的地址: + + ```java + C:\Users\Administrator\.IntelliJIdea2018.1\system\tomcat\Unnamed_javaweb-session-cookie\work\Catalina\localhost\ROOT\org\apache\jsp + ``` + + 发现页面转变成了Java程序! + + ![1568345948307](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568345948307.png) + + + +**浏览器向服务器发送请求,不管访问什么资源,其实都是在访问Servlet!** + +JSP最终也会被转换成为一个Java类! + +**JSP 本质上就是一个Servlet** + +```java +//初始化 + public void _jspInit() { + + } +//销毁 + public void _jspDestroy() { + } +//JSPService + public void _jspService(.HttpServletRequest request,HttpServletResponse response) + +``` + +1. 判断请求 + +2. 内置一些对象 + + ```java + final javax.servlet.jsp.PageContext pageContext; //页面上下文 + javax.servlet.http.HttpSession session = null; //session + final javax.servlet.ServletContext application; //applicationContext + final javax.servlet.ServletConfig config; //config + javax.servlet.jsp.JspWriter out = null; //out + final java.lang.Object page = this; //page:当前 + HttpServletRequest request //请求 + HttpServletResponse response //响应 + ``` + +3. 输出页面前增加的代码 + + ```java + response.setContentType("text/html"); //设置响应的页面类型 + pageContext = _jspxFactory.getPageContext(this, request, response, + null, true, 8192, true); + _jspx_page_context = pageContext; + application = pageContext.getServletContext(); + config = pageContext.getServletConfig(); + session = pageContext.getSession(); + out = pageContext.getOut(); + _jspx_out = out; + ``` + +4. 以上的这些个对象我们可以在JSP页面中直接使用! + +![1568347078207](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568347078207.png) + + + +在JSP页面中; + +只要是 JAVA代码就会原封不动的输出; + +如果是HTML代码,就会被转换为: + +```java +out.write("\r\n"); +``` + +这样的格式,输出到前端! + + + +### 8.3、JSP基础语法 + +任何语言都有自己的语法,JAVA中有,。 JSP 作为java技术的一种应用,它拥有一些自己扩充的语法(了解,知道即可!),Java所有语法都支持! + + + +#### **JSP表达式** + +```jsp + <%--JSP表达式 + 作用:用来将程序的输出,输出到客户端 + <%= 变量或者表达式%> + --%> + <%= new java.util.Date()%> +``` + + + +#### **jsp脚本片段** + +```jsp + + <%--jsp脚本片段--%> + <% + int sum = 0; + for (int i = 1; i <=100 ; i++) { + sum+=i; + } + out.println("

Sum="+sum+"

"); + %> + +``` + + + +**脚本片段的再实现** + +```jsp + <% + int x = 10; + out.println(x); + %> +

这是一个JSP文档

+ <% + int y = 2; + out.println(y); + %> + +
+ + + <%--在代码嵌入HTML元素--%> + <% + for (int i = 0; i < 5; i++) { + %> +

Hello,World <%=i%>

+ <% + } + %> +``` + + + +#### JSP声明 + +```jsp + <%! + static { + System.out.println("Loading Servlet!"); + } + + private int globalVar = 0; + + public void kuang(){ + System.out.println("进入了方法Kuang!"); + } + %> +``` + + + +JSP声明:会被编译到JSP生成Java的类中!其他的,就会被生成到_jspService方法中! + +在JSP,嵌入Java代码即可! + +```jsp +<%%> +<%=%> +<%!%> + +<%--注释--%> +``` + +JSP的注释,不会在客户端显示,HTML就会! + + + +### 8.4、JSP指令 + +```jsp +<%@page args.... %> +<%@include file=""%> + +<%--@include会将两个页面合二为一--%> + +<%@include file="common/header.jsp"%> +

网页主体

+ +<%@include file="common/footer.jsp"%> + +
+ + +<%--jSP标签 + jsp:include:拼接页面,本质还是三个 + --%> + +

网页主体

+ + +``` + + + +### 8.5、9大内置对象 + +- PageContext 存东西 +- Request 存东西 +- Response +- Session 存东西 +- Application 【SerlvetContext】 存东西 +- config 【SerlvetConfig】 +- out +- page ,不用了解 +- exception + +```java +pageContext.setAttribute("name1","秦疆1号"); //保存的数据只在一个页面中有效 +request.setAttribute("name2","秦疆2号"); //保存的数据只在一次请求中有效,请求转发会携带这个数据 +session.setAttribute("name3","秦疆3号"); //保存的数据只在一次会话中有效,从打开浏览器到关闭浏览器 +application.setAttribute("name4","秦疆4号"); //保存的数据只在服务器中有效,从打开服务器到关闭服务器 +``` + +request:客户端向服务器发送请求,产生的数据,用户看完就没用了,比如:新闻,用户看完没用的! + +session:客户端向服务器发送请求,产生的数据,用户用完一会还有用,比如:购物车; + +application:客户端向服务器发送请求,产生的数据,一个用户用完了,其他用户还可能使用,比如:聊天数据; + +### 8.6、JSP标签、JSTL标签、EL表达式 + +```xml + + + javax.servlet.jsp.jstl + jstl-api + 1.2 + + + + taglibs + standard + 1.1.2 + + +``` + +EL表达式: ${ } + +- **获取数据** +- **执行运算** +- **获取web开发的常用对象** + + + +**JSP标签** + +```jsp +<%--jsp:include--%> + +<%-- +http://localhost:8080/jsptag.jsp?name=kuangshen&age=12 +--%> + + + + + +``` + + + +**JSTL表达式** + +JSTL标签库的使用就是为了弥补HTML标签的不足;它自定义许多标签,可以供我们使用,标签的功能和Java代码一样! + +**格式化标签** + +**SQL标签** + +**XML 标签** + +**核心标签** (掌握部分) + +![1568362473764](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568362473764.png) + +**JSTL标签库使用步骤** + +- 引入对应的 taglib +- 使用其中的方法 +- **在Tomcat 也需要引入 jstl的包,否则会报错:JSTL解析错误** + +c:if + +```jsp + + Title + + + + +

if测试

+ +
+ +
+ <%-- + EL表达式获取表单中的数据 + ${param.参数名} + --%> + + +
+ +<%--判断如果提交的用户名是管理员,则登录成功--%> + + + + +<%--自闭合标签--%> + + + +``` + +c:choose c:when + +```jsp + + +<%--定义一个变量score,值为85--%> + + + + + 你的成绩为优秀 + + + 你的成绩为一般 + + + 你的成绩为良好 + + + 你的成绩为不及格 + + + + +``` + +c:forEach + +```jsp +<% + + ArrayList people = new ArrayList<>(); + people.add(0,"张三"); + people.add(1,"李四"); + people.add(2,"王五"); + people.add(3,"赵六"); + people.add(4,"田六"); + request.setAttribute("list",people); +%> + + +<%-- +var , 每一次遍历出来的变量 +items, 要遍历的对象 +begin, 哪里开始 +end, 到哪里 +step, 步长 +--%> + +
+
+ +
+ + +
+
+ +``` + +## 9、JavaBean + +实体类 + +JavaBean有特定的写法: + +- 必须要有一个无参构造 +- 属性必须私有化 +- 必须有对应的get/set方法; + +一般用来和数据库的字段做映射 ORM; + +ORM :对象关系映射 + +- 表--->类 +- 字段-->属性 +- 行记录---->对象 + +**people表** + +| id | name | age | address | +| ---- | ------- | ---- | ------- | +| 1 | 秦疆1号 | 3 | 西安 | +| 2 | 秦疆2号 | 18 | 西安 | +| 3 | 秦疆3号 | 100 | 西安 | + +```java +class People{ + private int id; + private String name; + private int id; + private String address; +} + +class A{ + new People(1,"秦疆1号",3,"西安"); + new People(2,"秦疆2号",3,"西安"); + new People(3,"秦疆3号",3,"西安"); +} +``` + + + +- 过滤器 +- 文件上传 +- 邮件发送 +- JDBC 复习 : 如何使用JDBC , JDBC crud, jdbc 事务 + + + +## 10、MVC三层架构 + +什么是MVC: Model view Controller 模型、视图、控制器 + +### 10.1、早些年 + +![1568423664332](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568423664332.png) + +用户直接访问控制层,控制层就可以直接操作数据库; + +```java +servlet--CRUD-->数据库 +弊端:程序十分臃肿,不利于维护 +servlet的代码中:处理请求、响应、视图跳转、处理JDBC、处理业务代码、处理逻辑代码 + +架构:没有什么是加一层解决不了的! +程序猿调用 +| +JDBC +| +Mysql Oracle SqlServer .... +``` + +### 10.2、MVC三层架构 + +![1568424227281](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568424227281.png) + + + +Model + +- 业务处理 :业务逻辑(Service) +- 数据持久层:CRUD (Dao) + +View + +- 展示数据 +- 提供链接发起Servlet请求 (a,form,img…) + +Controller (Servlet) + +- 接收用户的请求 :(req:请求参数、Session信息….) + +- 交给业务层处理对应的代码 + +- 控制视图的跳转 + + ```java + 登录--->接收用户的登录请求--->处理用户的请求(获取用户登录的参数,username,password)---->交给业务层处理登录业务(判断用户名密码是否正确:事务)--->Dao层查询用户名和密码是否正确-->数据库 + ``` + + + +## 11、Filter (重点) + +Filter:过滤器 ,用来过滤网站的数据; + +- 处理中文乱码 +- 登录验证…. + +![1568424858708](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568424858708.png) + +Filter开发步骤: + +1. 导包 + +2. 编写过滤器 + + 1. 导包不要错 + + ![1568425162525](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568425162525.png) + + 实现Filter接口,重写对应的方法即可 + + ```java + public class CharacterEncodingFilter implements Filter { + + //初始化:web服务器启动,就以及初始化了,随时等待过滤对象出现! + public void init(FilterConfig filterConfig) throws ServletException { + System.out.println("CharacterEncodingFilter初始化"); + } + + //Chain : 链 + /* + 1. 过滤中的所有代码,在过滤特定请求的时候都会执行 + 2. 必须要让过滤器继续同行 + chain.doFilter(request,response); + */ + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + request.setCharacterEncoding("utf-8"); + response.setCharacterEncoding("utf-8"); + response.setContentType("text/html;charset=UTF-8"); + + System.out.println("CharacterEncodingFilter执行前...."); + chain.doFilter(request,response); //让我们的请求继续走,如果不写,程序到这里就被拦截停止! + System.out.println("CharacterEncodingFilter执行后...."); + } + + //销毁:web服务器关闭的时候,过滤会销毁 + public void destroy() { + System.out.println("CharacterEncodingFilter销毁"); + } + } + + ``` + +3. 在web.xml中配置 Filter + + ```xml + + CharacterEncodingFilter + com.kuang.filter.CharacterEncodingFilter + + + CharacterEncodingFilter + + /servlet/* + + + ``` + + + +## 12、监听器 + +实现一个监听器的接口;(有N种) + +1. 编写一个监听器 + + 实现监听器的接口… + + ```java + //统计网站在线人数 : 统计session + public class OnlineCountListener implements HttpSessionListener { + + //创建session监听: 看你的一举一动 + //一旦创建Session就会触发一次这个事件! + public void sessionCreated(HttpSessionEvent se) { + ServletContext ctx = se.getSession().getServletContext(); + + System.out.println(se.getSession().getId()); + + Integer onlineCount = (Integer) ctx.getAttribute("OnlineCount"); + + if (onlineCount==null){ + onlineCount = new Integer(1); + }else { + int count = onlineCount.intValue(); + onlineCount = new Integer(count+1); + } + + ctx.setAttribute("OnlineCount",onlineCount); + + } + + //销毁session监听 + //一旦销毁Session就会触发一次这个事件! + public void sessionDestroyed(HttpSessionEvent se) { + ServletContext ctx = se.getSession().getServletContext(); + + Integer onlineCount = (Integer) ctx.getAttribute("OnlineCount"); + + if (onlineCount==null){ + onlineCount = new Integer(0); + }else { + int count = onlineCount.intValue(); + onlineCount = new Integer(count-1); + } + + ctx.setAttribute("OnlineCount",onlineCount); + + } + + + /* + Session销毁: + 1. 手动销毁 getSession().invalidate(); + 2. 自动销毁 + */ + } + + ``` + +2. web.xml中注册监听器 + + ```xml + + + com.kuang.listener.OnlineCountListener + + ``` + +3. 看情况是否使用! + + + +## 13、过滤器、监听器常见应用 + +**监听器:GUI编程中经常使用;** + +```java +public class TestPanel { + public static void main(String[] args) { + Frame frame = new Frame("中秋节快乐"); //新建一个窗体 + Panel panel = new Panel(null); //面板 + frame.setLayout(null); //设置窗体的布局 + + frame.setBounds(300,300,500,500); + frame.setBackground(new Color(0,0,255)); //设置背景颜色 + + panel.setBounds(50,50,300,300); + panel.setBackground(new Color(0,255,0)); //设置背景颜色 + + frame.add(panel); + + frame.setVisible(true); + + //监听事件,监听关闭事件 + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + super.windowClosing(e); + } + }); + + + } +} +``` + + + +用户登录之后才能进入主页!用户注销后就不能进入主页了! + +1. 用户登录之后,向Sesison中放入用户的数据 + +2. 进入主页的时候要判断用户是否已经登录;要求:在过滤器中实现! + + ```java + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) resp; + + if (request.getSession().getAttribute(Constant.USER_SESSION)==null){ + response.sendRedirect("/error.jsp"); + } + + chain.doFilter(request,response); + ``` + + + + +## 14、JDBC + +什么是JDBC : Java连接数据库! + +![1568439601825](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568439601825.png) + +需要jar包的支持: + +- java.sql +- javax.sql +- mysql-conneter-java… 连接驱动(必须要导入) + + + +**实验环境搭建** + +```sql + +CREATE TABLE users( + id INT PRIMARY KEY, + `name` VARCHAR(40), + `password` VARCHAR(40), + email VARCHAR(60), + birthday DATE +); + +INSERT INTO users(id,`name`,`password`,email,birthday) +VALUES(1,'张三','123456','zs@qq.com','2000-01-01'); +INSERT INTO users(id,`name`,`password`,email,birthday) +VALUES(2,'李四','123456','ls@qq.com','2000-01-01'); +INSERT INTO users(id,`name`,`password`,email,birthday) +VALUES(3,'王五','123456','ww@qq.com','2000-01-01'); + + +SELECT * FROM users; + +``` + + + +导入数据库依赖 + +```xml + + + mysql + mysql-connector-java + 5.1.47 + +``` + +IDEA中连接数据库: + +![1568440926845](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568440926845.png) + + + +**JDBC 固定步骤:** + +1. 加载驱动 +2. 连接数据库,代表数据库 +3. 向数据库发送SQL的对象Statement : CRUD +4. 编写SQL (根据业务,不同的SQL) +5. 执行SQL +6. 关闭连接 + +```java +public class TestJdbc { + public static void main(String[] args) throws ClassNotFoundException, SQLException { + //配置信息 + //useUnicode=true&characterEncoding=utf-8 解决中文乱码 + String url="jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8"; + String username = "root"; + String password = "123456"; + + //1.加载驱动 + Class.forName("com.mysql.jdbc.Driver"); + //2.连接数据库,代表数据库 + Connection connection = DriverManager.getConnection(url, username, password); + + //3.向数据库发送SQL的对象Statement,PreparedStatement : CRUD + Statement statement = connection.createStatement(); + + //4.编写SQL + String sql = "select * from users"; + + //5.执行查询SQL,返回一个 ResultSet : 结果集 + ResultSet rs = statement.executeQuery(sql); + + while (rs.next()){ + System.out.println("id="+rs.getObject("id")); + System.out.println("name="+rs.getObject("name")); + System.out.println("password="+rs.getObject("password")); + System.out.println("email="+rs.getObject("email")); + System.out.println("birthday="+rs.getObject("birthday")); + } + + //6.关闭连接,释放资源(一定要做) 先开后关 + rs.close(); + statement.close(); + connection.close(); + } +} + +``` + + + +**预编译SQL** + +```java +public class TestJDBC2 { + public static void main(String[] args) throws Exception { + //配置信息 + //useUnicode=true&characterEncoding=utf-8 解决中文乱码 + String url="jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8"; + String username = "root"; + String password = "123456"; + + //1.加载驱动 + Class.forName("com.mysql.jdbc.Driver"); + //2.连接数据库,代表数据库 + Connection connection = DriverManager.getConnection(url, username, password); + + //3.编写SQL + String sql = "insert into users(id, name, password, email, birthday) values (?,?,?,?,?);"; + + //4.预编译 + PreparedStatement preparedStatement = connection.prepareStatement(sql); + + preparedStatement.setInt(1,2);//给第一个占位符? 的值赋值为1; + preparedStatement.setString(2,"狂神说Java");//给第二个占位符? 的值赋值为狂神说Java; + preparedStatement.setString(3,"123456");//给第三个占位符? 的值赋值为123456; + preparedStatement.setString(4,"24736743@qq.com");//给第四个占位符? 的值赋值为1; + preparedStatement.setDate(5,new Date(new java.util.Date().getTime()));//给第五个占位符? 的值赋值为new Date(new java.util.Date().getTime()); + + //5.执行SQL + int i = preparedStatement.executeUpdate(); + + if (i>0){ + System.out.println("插入成功@"); + } + + //6.关闭连接,释放资源(一定要做) 先开后关 + preparedStatement.close(); + connection.close(); + } +} + +``` + + + +**事务** + +要么都成功,要么都失败! + +ACID原则:保证数据的安全。 + +```java +开启事务 +事务提交 commit() +事务回滚 rollback() +关闭事务 + +转账: +A:1000 +B:1000 + +A(900) --100--> B(1100) +``` + + + +**Junit单元测试** + +依赖 + +```xml + + + junit + junit + 4.12 + +``` + +简单使用 + +@Test注解只有在方法上有效,只要加了这个注解的方法,就可以直接运行! + +```java +@Test +public void test(){ + System.out.println("Hello"); +} +``` + +![1568442261610](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568442261610.png) + +失败的时候是红色: + +![1568442289597](https://cdn.jsdelivr.net/gh/oddfar/static/img/JavaWeb.assets/1568442289597.png) + + + +**搭建一个环境** + +```sql +CREATE TABLE account( + id INT PRIMARY KEY AUTO_INCREMENT, + `name` VARCHAR(40), + money FLOAT +); + +INSERT INTO account(`name`,money) VALUES('A',1000); +INSERT INTO account(`name`,money) VALUES('B',1000); +INSERT INTO account(`name`,money) VALUES('C',1000); +``` + +```java + @Test + public void test() { + //配置信息 + //useUnicode=true&characterEncoding=utf-8 解决中文乱码 + String url="jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8"; + String username = "root"; + String password = "123456"; + + Connection connection = null; + + //1.加载驱动 + try { + Class.forName("com.mysql.jdbc.Driver"); + //2.连接数据库,代表数据库 + connection = DriverManager.getConnection(url, username, password); + + //3.通知数据库开启事务,false 开启 + connection.setAutoCommit(false); + + String sql = "update account set money = money-100 where name = 'A'"; + connection.prepareStatement(sql).executeUpdate(); + + //制造错误 + //int i = 1/0; + + String sql2 = "update account set money = money+100 where name = 'B'"; + connection.prepareStatement(sql2).executeUpdate(); + + connection.commit();//以上两条SQL都执行成功了,就提交事务! + System.out.println("success"); + } catch (Exception e) { + try { + //如果出现异常,就通知数据库回滚事务 + connection.rollback(); + } catch (SQLException e1) { + e1.printStackTrace(); + } + e.printStackTrace(); + }finally { + try { + connection.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } +``` + +## 15、数据库连接池 + +**概念:其实就是一个容器(集合),存放数据库连接的容器。** +当系统初始化好后,容器被创建,容器中会申请一些连接对象,当用户来访问数据库时,从容器中获取连接对象,用户访问完之后,会将连接对象归还给容器。 +**好处:** + +1. 节约资源 +2. 用户访问高效 + +**实现:** +标准接口:`DataSource javax.sql`包下的 + +1. 方法: + +>获取连接:`getConnection()` +>归还连接:`Connection.close()`。 +>如果连接对象Connection是从连接池中获取的,那么调用`Connection.close()`方法,则不会再关闭连接了。而是归还连接 + +2. 一般我们不去实现它,有数据库厂商来实现 + +>1. C3P0:数据库连接池技术 +>2. Druid:数据库连接池实现技术,由阿里巴巴提供的 + + +**C3P0:数据库连接池技术** + +- 步骤: + +1. 导入jar包 (两个) c3p0-0.9.5.2.jar mchange-commons-java-0.2.12.jar , + *不要忘记导入数据库驱动 `jar` 包* +2. 定义配置文件: + +>名称: `c3p0.properties` 或者 `c3p0-config.xml` +>路径:直接将文件放在src目录下即可。 + +3. 创建核心对象 数据库连接池对象 `ComboPooledDataSource()` +4. 获取连接:`getConnection()` + +- 代码: + +```java +//1.创建数据库连接池对象 +DataSource ds = new ComboPooledDataSource(); +//2. 获取连接对象 +Connection conn = ds.getConnection(); +``` + +**Druid:数据库连接池实现技术,由阿里巴巴提供的** + +- 步骤: + +1. 导入jar包 `druid-1.0.9.jar` +2. 定义配置文件: + * 是`properties`形式的* + * 可以叫任意名称,可以放在任意目录下* +3. 加载配置文件。`Properties` +4. 获取数据库连接池对象:通过工厂来来获取 `DruidDataSourceFactory()` +5. 获取连接:`getConnection()` + +- 代码: + +``` +//3.加载配置文件 +Properties pro = new Properties(); +InputStream is = DruidDemo.class.getClassLoader().getResourceAsStream("druid.properties"); +pro.load(is); +//4.获取连接池对象 +DataSource ds = DruidDataSourceFactory.createDataSource(pro); +//5.获取连接 +Connection conn = ds.getConnection(); +``` + +**定义工具类** + +1. 定义一个类 JDBCUtils +2. 提供静态代码块加载配置文件,初始化连接池对象 +3. 提供方法 + +> 1. 获取连接方法:通过数据库连接池获取连接 +> 2. 释放资源 +> 3. 获取连接池的方法 + + +**代码:** + +```Java +public class JDBCUtils { + + //1.定义成员变量 DataSource + private static DataSource ds ; + + static{ + try { + //1.加载配置文件 + Properties pro = new Properties(); + pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties")); + //2.获取DataSource + ds = DruidDataSourceFactory.createDataSource(pro); + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 获取连接 + */ + public static Connection getConnection() throws SQLException { + return ds.getConnection(); + } + + /** + * 释放资源 + */ + public static void close(Statement stmt,Connection conn){ + /* if(stmt != null){ + try { + stmt.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + if(conn != null){ + try { + conn.close();//归还连接 + } catch (SQLException e) { + e.printStackTrace(); + } + }*/ + + close(null,stmt,conn); + } + + + public static void close(ResultSet rs , Statement stmt, Connection conn){ + + if(rs != null){ + try { + rs.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + if(stmt != null){ + try { + stmt.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + if(conn != null){ + try { + conn.close();//归还连接 + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + /** + * 获取连接池方法 + */ + + public static DataSource getDataSource(){ + return ds; + } +} +``` \ No newline at end of file diff --git "a/docs/02.Web/01.Html5/02.\346\200\273\350\247\210.md" "b/docs/02.Web/01.Html5/02.\346\200\273\350\247\210.md" new file mode 100644 index 00000000..4f35a28c --- /dev/null +++ "b/docs/02.Web/01.Html5/02.\346\200\273\350\247\210.md" @@ -0,0 +1,13 @@ +--- +title: 总览 +permalink: /html5/overview +date: 2021-05-09 09:50:58 +--- + + + + + +# 总览 + +html5 \ No newline at end of file diff --git "a/docs/02.Web/02.CSS/02.\346\200\273\350\247\210.md" "b/docs/02.Web/02.CSS/02.\346\200\273\350\247\210.md" new file mode 100644 index 00000000..b9969cf2 --- /dev/null +++ "b/docs/02.Web/02.CSS/02.\346\200\273\350\247\210.md" @@ -0,0 +1,16 @@ +--- +title: 总览 +permalink: /css/overview +categories: + - java + - java-web +date: 2021-05-09 09:53:16 +--- + + + + + +# 总览 + +css \ No newline at end of file diff --git "a/docs/02.Web/03.JavaScript/02.\346\200\273\350\247\210.md" "b/docs/02.Web/03.JavaScript/02.\346\200\273\350\247\210.md" new file mode 100644 index 00000000..f23a8340 --- /dev/null +++ "b/docs/02.Web/03.JavaScript/02.\346\200\273\350\247\210.md" @@ -0,0 +1,14 @@ +--- +title: 总览 +permalink: /javascript/overview +date: 2021-05-09 09:53:35 +--- + + + + + +# 总览 + +JavaScript + diff --git "a/docs/02.Web/10.vue/02.\346\200\273\350\247\210.md" "b/docs/02.Web/10.vue/02.\346\200\273\350\247\210.md" new file mode 100644 index 00000000..5f47d9fa --- /dev/null +++ "b/docs/02.Web/10.vue/02.\346\200\273\350\247\210.md" @@ -0,0 +1,14 @@ +--- +title: 总览 +permalink: /vue/overview +date: 2021-05-09 09:53:35 +--- + + + + + +# 总览 + +vue + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/02.MySQL - \345\211\215\350\250\200.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/02.MySQL - \345\211\215\350\250\200.md" new file mode 100644 index 00000000..d2064489 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/02.MySQL - \345\211\215\350\250\200.md" @@ -0,0 +1,42 @@ +--- +title: MySQL - 前言 +permalink: /mysql/preface/ +date: 2021-05-20 09:55:12 +--- + +**MySQL入门:** + +MySQL基础笔记可看 [菜鸟教程](https://www.runoob.com/mysql/mysql-tutorial.html) + +mysql基础视频教程 bilibili 上搜即可 + +--- + +**MySQL进阶:** + +- **尚硅谷MySQL数据库高级** + + 视频:https://www.bilibili.com/video/BV1KW411u7vy + + 笔记链接:https://pan.baidu.com/s/1GUzPFVG3Je9uT419rHE8MQ + + 提取码:ybfi + +- **黑马程序员2020最新MySQL高级教程** + + 视频:https://www.bilibili.com/video/BV1UQ4y1P7Xr + + 笔记:https://gitee.com/yooome/netty-study + + + + +我刚开始看的是尚硅谷的,看了一半跑去看黑马的了 + +这两教程内容都差不多,黑马的更多一些 + + + +推荐书籍:《深入浅出mysql》 + +视频内容围绕着这本书上内容讲的 \ No newline at end of file diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/04.MySQL - \351\200\273\350\276\221\346\236\266\346\236\204.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/04.MySQL - \351\200\273\350\276\221\346\236\266\346\236\204.md" new file mode 100644 index 00000000..a9d1c852 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/04.MySQL - \351\200\273\350\276\221\346\236\266\346\236\204.md" @@ -0,0 +1,435 @@ +--- +title: MySQL - 逻辑架构简介 +permalink: /mysql/logic-framework/ +date: 2021-05-20 09:55:12 +--- + + + + + +- [Mysql 逻辑架构简介](#mysql-%E9%80%BB%E8%BE%91%E6%9E%B6%E6%9E%84%E7%AE%80%E4%BB%8B) + - [整体架构图](#%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E5%9B%BE) + - [连接层](#%E8%BF%9E%E6%8E%A5%E5%B1%82) + - [服务层](#%E6%9C%8D%E5%8A%A1%E5%B1%82) + - [引擎层](#%E5%BC%95%E6%93%8E%E5%B1%82) + - [存储层](#%E5%AD%98%E5%82%A8%E5%B1%82) + - [show profile](#show-profile) + - [大致的查询流程](#%E5%A4%A7%E8%87%B4%E7%9A%84%E6%9F%A5%E8%AF%A2%E6%B5%81%E7%A8%8B) + - [SQL的执行顺序](#sql%E7%9A%84%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F) +- [存储引擎](#%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E) + - [存储引擎概述](#%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E6%A6%82%E8%BF%B0) + - [各种存储引擎特性](#%E5%90%84%E7%A7%8D%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%89%B9%E6%80%A7) + - [InnoDB](#innodb) + - [MyISAM](#myisam) + - [MEMORY](#memory) + - [MERGE](#merge) +- [SQL 预热](#sql-%E9%A2%84%E7%83%AD) + + + + + +## Mysql 逻辑架构简介 + +### 整体架构图 + +![img](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/540235-20160927083854563-2139392246.jpg) + +和其它数据库相比,MySQL 有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离。这种架构可 以根据业务的需求和实际需要选择合适的存储引擎。 + +**各层介绍:** + +#### 连接层 + +最上层是一些客户端和连接服务,包含本地 sock 通信和大多数基于客户端/服务端工具实现的类似于 tcp/ip 的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证 安全接入的客户端提供线程。同样在该层上可以实现基于 SSL 的安全链接。服务器也会为安全接入的每个客户端验 证它所具有的操作权限。 + +#### 服务层 + +- Management Serveices & Utilities + + 系统管理和控制工具 + +- SQL Interface + + SQL 接口。接受用户的 SQL 命令,并且返回用户需要查询的结果。比如 select from 就是调用 SQL Interface + +- Parser + + 解析器。 SQL 命令传递到解析器的时候会被解析器验证和解析 + +- Optimizer + + 查询优化器。 SQL 语句在查询之前会使用查询优化器对查询进行优化,比如有 where 条件时,优化器来决定先投影还是先过滤。 + +- Cache 和 Buffer + + 查询缓存。如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key 缓存, 权限缓存等 + + 注:mysql 8.X 取消了查询缓存 + +#### 引擎层 + +存储引擎层,存储引擎真正的负责了 MySQL 中数据的存储和提取,服务器通过 API 与存储引擎进行通信。不同 的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。 + +#### 存储层 + +数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎的交互。 + + + +### show profile + +利用 show profile 可以查看 sql 的执行周期! + +**开启 profile** + +查看 profile 是否开启:show variables like '%profiling%' + +```sh +mysql> show variables like '%profiling%'; ++------------------------+-------+ +| Variable_name | Value | ++------------------------+-------+ +| have_profiling | YES | +| profiling | OFF | +| profiling_history_size | 15 | ++------------------------+-------+ +3 rows in set (0.01 sec) +``` + +如果没有开启,可以执行 set profiling=1 开启! + +**使用 profile** + +执行 `show profiles;` 命令,可以查看最近的几次查询。 + +根据 `Query_ID`,可以进一步执行 `show profile cpu,block io for query Query_id` 来查看 sql 的具体执行步骤。 + + + +### 大致的查询流程 + +mysql 的查询流程大致是: + +mysql 客户端通过协议与 mysql 服务器建连接,发送查询语句,先检查查询缓存,如果命中,直接返回结果, 否则进行语句解析,也就是说,在解析查询之前,服务器会先访问查询缓存(query cache)——它存储 SELECT 语句以及 相应的查询结果集。如果某个查询结果已经位于缓存中,服务器就不会再对查询进行解析、优化、以及执行。它仅仅将缓存中的结果返回给用户即可,这将大大提高系统的性能。 + +语法解析器和预处理:首先 mysql 通过关键字将 SQL 语句进行解析,并生成一颗对应的“解析树”。mysql 解析器将使用 mysql 语法规则验证和解析查询;预处理器则根据一些 mysql 规则进一步检查解析数是否合法。 + +查询优化器当解析树被认为是合法的了,并且由优化器将其转化成执行计划。一条查询可以有很多种执行方式, 最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。 + +然后,mysql 默认使用的 BTREE 索引,并且一个大致方向是:无论怎么折腾 sql,至少在目前来说,mysql 最多只用到表中的一个索引。 + + + +### SQL的执行顺序 + +手写的顺序: + +![image-20210520161848827](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210520161848827.png) + +真正执行的顺序: + +​ 随着 Mysql 版本的更新换代,其优化器也在不断的升级,优化器会分析不同执行顺序产生的性能消耗不同而动 态调整执行顺序。下面是经常出现的查询顺序: + +![image-20210520161931864](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210520161931864.png) + +![image-20210520164022838](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210520164022838.png) + + + +## 存储引擎 + +### 存储引擎概述 + +和大多数的数据库不同, MySQL中有一个存储引擎的概念, 针对不同的存储需求可以选择最优的存储引擎。 + +​ 存储引擎就是存储数据,建立索引,更新查询数据等等技术的实现方式 。存储引擎是基于表的,而不是基于库的。所以存储引擎也可被称为表类型。 + +​ Oracle,SqlServer等数据库只有一种存储引擎。MySQL提供了插件式的存储引擎架构。所以MySQL存在多种存储引擎,可以根据需要使用相应引擎,或者编写存储引擎。 + +​ MySQL5.0支持的存储引擎包含 : InnoDB 、MyISAM 、BDB、MEMORY、MERGE、EXAMPLE、NDB Cluster、ARCHIVE、CSV、BLACKHOLE、FEDERATED等,其中InnoDB和BDB提供事务安全表,其他存储引擎是非事务安全表。 + +通过指令查询当前数据库支持的存储引擎 : + +```sql +show engines +``` + +![1551186043529](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1551186043529.png) + + + +创建新表时如果不指定存储引擎,那么系统就会使用默认的存储引擎,MySQL5.5之前的默认存储引擎是MyISAM,5.5之后就改为了InnoDB。 + +查看Mysql数据库默认的存储引擎 , 指令 : + +```sql +show variables like '%storage_engine%' ; +``` + +![1556086372754](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556086372754.png) + +### 各种存储引擎特性 + +下面重点介绍几种常用的存储引擎, 并对比各个存储引擎之间的区别, 如下表所示 : + +| 特点 | InnoDB | MyISAM | MEMORY | MERGE | NDB | +| ------------ | -------------------- | -------- | ------ | ----- | ---- | +| 存储限制 | 64TB | 有 | 有 | 没有 | 有 | +| 事务安全 | **支持** | | | | | +| 锁机制 | **行锁(适合高并发)** | **表锁** | 表锁 | 表锁 | 行锁 | +| B树索引 | 支持 | 支持 | 支持 | 支持 | 支持 | +| 哈希索引 | | | 支持 | | | +| 全文索引 | 支持(5.6版本之后) | 支持 | | | | +| 集群索引 | 支持 | | | | | +| 数据索引 | 支持 | | 支持 | | 支持 | +| 索引缓存 | 支持 | 支持 | 支持 | 支持 | 支持 | +| 数据可压缩 | | 支持 | | | | +| 空间使用 | 高 | 低 | N/A | 低 | 低 | +| 内存使用 | 高 | 低 | 中等 | 低 | 高 | +| 批量插入速度 | 低 | 高 | 高 | 高 | 高 | +| 支持外键 | **支持** | | | | | + +下面我们将重点介绍最长使用的两种存储引擎: InnoDB、MyISAM , 另外两种 MEMORY、MERGE , 了解即可。 + +#### InnoDB + +InnoDB 存储引擎是 Mysql 的默认存储引擎。 + +InnoDB存储引擎提供了具有提交、回滚、崩溃恢复能力的事务安全。但是对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引。 + +InnoDB 存储引擎不同于其他存储引擎的特点 : + +- 事务控制 + + ** + + ```sql + create table goods_innodb( + id int NOT NULL AUTO_INCREMENT, + name varchar(20) NOT NULL, + primary key(id) + )ENGINE=innodb DEFAULT CHARSET=utf8; + ``` + + ```sql + #1 + start transaction; + #2 + insert into goods_innodb(id,name)values(null,'Meta20'); + #3 + commit; + ``` + + ![1556075130115](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556075130115.png) + + 测试发现在 InnoDB 中是存在事务的 ; + +- 外键约束 + + MySQL支持外键的存储引擎只有 InnoDB , 在创建外键的时候, 要求父表必须有对应的索引 ,子表在创建外键的时候,也会自动的创建对应的索引。 + + 下面两张表中 , country_innodb 是父表 , country_id 为主键索引,city_innodb 表是子表,country_id 字段为外键,对应于 country_innodb 表的主键 country_id 。 + + ```sql + create table country_innodb( + country_id int NOT NULL AUTO_INCREMENT, + country_name varchar(100) NOT NULL, + primary key(country_id) + )ENGINE=InnoDB DEFAULT CHARSET=utf8; + + + create table city_innodb( + city_id int NOT NULL AUTO_INCREMENT, + city_name varchar(50) NOT NULL, + country_id int NOT NULL, + primary key(city_id), + key idx_fk_country_id(country_id), + CONSTRAINT `fk_city_country` FOREIGN KEY(country_id) REFERENCES country_innodb(country_id) ON DELETE RESTRICT ON UPDATE CASCADE + )ENGINE=InnoDB DEFAULT CHARSET=utf8; + + + + insert into country_innodb values(null,'China'),(null,'America'),(null,'Japan'); + insert into city_innodb values(null,'Xian',1),(null,'NewYork',2),(null,'BeiJing',1); + + ``` + + 在创建索引时, 可以指定在删除、更新父表时,对子表进行的相应操作,包括 RESTRICT、CASCADE、SET NULL 和 NO ACTION。 + + RESTRICT 和NO ACTION 相同, 是指限制在子表有关联记录的情况下, 父表不能更新; + + CASCADE 表示父表在更新或者删除时,更新或者删除子表对应的记录; + + SET NULL 则表示父表在更新或者删除的时候,子表的对应字段被SET NULL 。 + + 针对上面创建的两个表, 子表的外键指定是ON DELETE RESTRICT ON UPDATE CASCADE 方式的, 那么在主表删除记录的时候, 如果子表有对应记录, 则不允许删除, 主表在更新记录的时候, 如果子表有对应记录, 则子表对应更新 。 + + + + 表中数据如下图所示 : + + ![1556087540767](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556087540767.png) + + 查看外键信息 + + ```sql + show create table city_innodb ; + ``` + + ![1556087611295](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556087611295.png) + + 删除 country_id为 1 的 country 数据,有外键时会删除失败 + + ![1556087719145](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556087719145.png) + + 更新主表country表的字段 country_id 时 + + ```sql + update country_innodb set country_id = 100 where country_id = 1; + ``` + + ![1556087759615](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556087759615.png) + + 更新后, 子表的数据信息为 : + + ![1556087793738](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556087793738.png) + + + +- **存储方式** + + InnoDB 存储表和索引有以下两种方式 : + + ①. 使用共享表空间存储, 这种方式创建的表的表结构保存在 `.frm`文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件。 + + ②. 使用多表空间存储, 这种方式创建的表的表结构仍然存在 `.frm` 文件中,但是每个表的数据和索引单独保存在 `.ibd` 中。 + + ![1556075336630](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556075336630.png) + + + +#### MyISAM + +MyISAM 不支持事务、也不支持外键,其优势是访问的速度快,对事务的完整性没有要求或者以SELECT、INSERT为主的应用基本上都可以使用这个引擎来创建表 。有以下两个比较重要的特点: + +- 不支持事务 + + ```sql + create table goods_myisam( + id int NOT NULL AUTO_INCREMENT, + name varchar(20) NOT NULL, + primary key(id) + )ENGINE=myisam DEFAULT CHARSET=utf8; + ``` + + ![1551347590309](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1551347590309.png) + + 通过测试,我们发现,在MyISAM存储引擎中,是没有事务控制的 ; + + + +- 文件存储方式 + + 每个MyISAM在磁盘上存储成3个文件,其文件名都和表名相同,但拓展名分别是 : + + .frm (存储表定义); + + .MYD(MYData , 存储数据); + + .MYI(MYIndex , 存储索引); + + ![1556075073836](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556075073836.png) + + + +#### MEMORY + +​ Memory存储引擎将表的数据存放在内存中。每个MEMORY表实际对应一个磁盘文件,格式是.frm ,该文件中只存储表的结构,而其数据文件,都是存储在内存中,这样有利于数据的快速处理,提高整个表的效率。MEMORY 类型的表访问非常地快,因为他的数据是存放在内存中的,并且默认使用HASH索引 , 但是服务一旦关闭,表中的数据就会丢失。 + +#### MERGE + +​ MERGE存储引擎是一组MyISAM表的组合,这些MyISAM表必须结构完全相同,MERGE表本身并没有存储数据,对MERGE类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的MyISAM表进行的。 + +​ 对于MERGE类型表的插入操作,是通过INSERT_METHOD子句定义插入的表,可以有3个不同的值,使用FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上,不定义这个子句或者定义为NO,表示不能对这个MERGE表执行插入操作。 + +​ 可以对MERGE表进行DROP操作,但是这个操作只是删除MERGE表的定义,对内部的表是没有任何影响的。 + + + +![1556076359503](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556076359503.png) + +**下面是一个创建和使用MERGE表的示例 :** + +1). 创建3个测试表 order_1990, order_1991, order_all , 其中 order_all 是前两个表的 MERGE 表 : + +```sql +create table order_1990( + order_id int , + order_money double(10,2), + order_address varchar(50), + primary key (order_id) +)engine = myisam default charset=utf8; + + +create table order_1991( + order_id int , + order_money double(10,2), + order_address varchar(50), + primary key (order_id) +)engine = myisam default charset=utf8; + + +create table order_all( + order_id int , + order_money double(10,2), + order_address varchar(50), + primary key (order_id) +)engine = merge union = (order_1990,order_1991) INSERT_METHOD=LAST default charset=utf8; +``` + +2). 分别向两张表中插入记录 + +```sql +insert into order_1990 values(1,100.0,'北京'); +insert into order_1990 values(2,100.0,'上海'); + +insert into order_1991 values(10,200.0,'北京'); +insert into order_1991 values(11,200.0,'上海'); +``` + +3). 查询3张表中的数据。 + +order_1990中的数据 : + +![1551408083254](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1551408083254.png) + +order_1991中的数据 : + +![1551408133323](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1551408133323.png) + +order_all中的数据 : + +![1551408216185](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1551408216185.png) + +4). 往order_all中插入一条记录 ,由于在MERGE表定义时,INSERT_METHOD 选择的是LAST,那么插入的数据会想最后一张表中插入。 + +```sql +insert into order_all values(100,10000.0,'西安'); +``` + +![1551408519889](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1551408519889.png) + + + +## SQL 预热 + +常见的 Join 查询图 + +![image-20210520162731111](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210520162731111.png) + + + + + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/06.MySQL - \347\264\242\345\274\225.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/06.MySQL - \347\264\242\345\274\225.md" new file mode 100644 index 00000000..5ebcbe2b --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/06.MySQL - \347\264\242\345\274\225.md" @@ -0,0 +1,436 @@ +--- +title: MySQL - 索引介绍 +permalink: /mysql/index/ +date: 2021-05-20 20:51:46 +--- + + + + + +- [索引的概念](#%E7%B4%A2%E5%BC%95%E7%9A%84%E6%A6%82%E5%BF%B5) +- [推荐文章](#%E6%8E%A8%E8%8D%90%E6%96%87%E7%AB%A0) +- [索引结构](#%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84) + - [Btree 索引结构](#btree-%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84) + - [演变过程](#%E6%BC%94%E5%8F%98%E8%BF%87%E7%A8%8B) + - [其他](#%E5%85%B6%E4%BB%96) + - [B+tree 结构](#btree-%E7%BB%93%E6%9E%84) + - [MySQL中的B+Tree](#mysql%E4%B8%AD%E7%9A%84btree) + - [聚簇索引和非聚簇索引](#%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95%E5%92%8C%E9%9D%9E%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95) + - [时间复杂度](#%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6) +- [索引分类](#%E7%B4%A2%E5%BC%95%E5%88%86%E7%B1%BB) + - [单值索引](#%E5%8D%95%E5%80%BC%E7%B4%A2%E5%BC%95) + - [唯一索引](#%E5%94%AF%E4%B8%80%E7%B4%A2%E5%BC%95) + - [复合索引](#%E5%A4%8D%E5%90%88%E7%B4%A2%E5%BC%95) + - [主键索引](#%E4%B8%BB%E9%94%AE%E7%B4%A2%E5%BC%95) + - [索引基本语法](#%E7%B4%A2%E5%BC%95%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95) +- [索引设计原则](#%E7%B4%A2%E5%BC%95%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99) + + + +## 索引的概念 + +索引(Index)是帮助 MySQL 高效获取数据的数据结构,可以简单理解为**排好序的快速查找数据结构**。 + +在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引 + + + +![1555902055367](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555902055367.png) + + + +左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找快速获取到相应数据。 + +一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。索引是数据库中用来提高性能的最常用的工具。 + + + +**索引优缺点** + +优势: + +- 提高数据检索的效率,降低数据库的IO成本。 +- 数据排序,降低CPU消耗 + +劣势: + +- 虽然索引大大提高了查询速度,同时却会降低更新表的速度 + + 如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,都会调整因更新所带来的键值变化后的索引信息。 + +- 索引本质也是一张表,保存着索引字段和指向实际记录的指针,所以也要占用数据库空间,一般而言,索引表占用的空间是数据表的1.5倍 + + + + + +## 推荐文章 + +- [MySQL 索引原理 图文讲解](https://zhuanlan.zhihu.com/p/359306500) + + 涉及到树相关数据结构知识。 + +- [BTree和B+Tree](https://www.jianshu.com/p/ac12d2c83708) + + 详细介绍 + +## 索引结构 + +### Btree 索引结构 + +黑马教程:https://www.bilibili.com/video/BV1UQ4y1P7Xr?p=6 + +**Btree又可以写成B-tree**(B-Tree,并不是B“减”树,横杠为连接符,容易被误导) + +BTree又叫多路平衡搜索树,一颗 **m** 叉的BTree特性如下: + +- 树中每个节点最多包含m个孩子。 +- 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子。 +- 若根节点不是叶子节点,则至少有两个孩子。 +- 所有的叶子节点都在同一层。 +- 每个非叶子节点由 n 个 key 与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 + +celi():向上取整,例如celi(2.5)=3 + + + +#### 演变过程 + +以5叉 BTree 为例,key的数量:公式推导 [ceil(m/2)-1] <= n <= m-1。所以 2 <= n <=4 。 + +当 n>4 时,中间节点分裂到父节点,两边节点分裂。 + +插入 C N G A H E K Q M F W L T Z D P R X Y S 数据为例。(插入时,按照ABCD..顺序) + +1). 插入前4个字母 C N G A + +![1555944126588](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555944126588.png) + +2). 插入H,n>4,中间元素G字母向上分裂到新的节点 + +![1555944549825](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555944549825.png) + +3). 插入E,K,Q不需要分裂 + +![1555944596893](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555944596893.png) + +4). 插入M,中间元素M字母向上分裂到父节点G + +(M 在 K、N中间,BTree最多含有n-1个key,这里即4个key,5个指针) + +![1555944652560](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555944652560.png) + +5). 插入F,W,L,T不需要分裂 + +![1555944686928](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555944686928.png) + +6). 插入Z,中间元素T向上分裂到父节点中 + +(插入Z后是,NQTWZ,把中间T提出来,方便更快的查询,所以不提出Z) + +![1555944713486](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555944713486.png) + +7). 插入D,中间元素D向上分裂到父节点中。然后插入P,R,X,Y不需要分裂 + +![1555944749984](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555944749984.png) + +8). 最后插入S(R后面),NPQR节点n>5,中间节点Q向上分裂,但分裂后父节点DGMT的n>5,中间节点M向上分裂 + +![1555944848294](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555944848294.png) + +到此,该BTREE树就已经构建完成了, BTREE树 和 二叉树 相比, 查询数据的效率更高, 因为对于相同的数据量来说,BTREE的层级结构比二叉树小,因此搜索速度快。 + + + +#### 其他 + +![image-20210520190717898](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210520190717898.png) + +**初始化介绍** + + + +一颗 b 树,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色 所示), + +如磁盘块 1 包含数据项 17 和 35,包含指针 P1、P2、P3, +P1 表示小于 17 的磁盘块,P2 表示在 17 和 35 之间的磁盘块,P3 表示大于 35 的磁盘块。 +真实的数据存在于叶子节点即 3、5、9、10、13、15、28、29、36、60、75、79、90、99。 +非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如 17、35 并不真实存在于数据表中 + +**查找过程** + +如果要查找数据项 29,那么首先会把磁盘块 1 由磁盘加载到内存,此时发生一次 IO,在内存中用二分查找确定 29 在 17 和 35 之间,锁定磁盘块 1 的 P2 指针,内存时间因为非常短(相比磁盘的 IO)可以忽略不计,通过磁盘块 1 的 P2 指针的磁盘地址把磁盘块 3 由磁盘加载到内存,发生第二次 IO,29 在 26 和 30 之间,锁定磁盘块 3 的 P2 指 针,通过指针加载磁盘块 8 到内存,发生第三次 IO,同时内存中做二分查找找到 29,结束查询,总计三次 IO。 + + + +真实的情况是,3 层的 b+树可以表示上百万的数据,如果上百万的数据查找只需要三次 IO,性能提高将是巨大的, 如果没有索引,每个数据项都要发生一次 IO,那么总共需要百万次的 IO,显然成本非常非常高。 + + + +### B+tree 结构 + +![1555906287178](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/00001.jpg) + +B+Tree为BTree的变种,B+Tree与BTree的区别为: + +1). n叉B+Tree最多含有n个key,而BTree最多含有n-1个key。 + +2). B+Tree的叶子节点保存所有的key信息,依key大小顺序排列。 + +3). 所有的非叶子节点都可以看作是key的索引部分。 + + + +- B-树的关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+树的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。 +- 在 B-树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而 B+树中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。从这个角度看 B- 树的性能好像要比 B+树好,而在实际应用中却是 B+树的性能要好些。因为 B+树的非叶子节点不存放实际的数据, 这样每个节点可容纳的元素个数比 B-树多,树高比 B-树小,这样带来的好处是减少磁盘访问次数。尽管 B+树找到 一个记录所需的比较次数要比 B-树多,但是一次磁盘访问的时间相当于成百上千次内存比较的时间,因此实际中 B+树的性能可能还会好些,而且 B+树的叶子节点使用指针连接在一起,方便顺序遍历(例如查看一个目录下的所有 文件,一个表中的所有记录等),这也是很多数据库和文件系统使用 B+树的缘故。 + +**为什么B+树比 B-树更适合索引?** + +- B+树的磁盘读写代价更低 + + B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对 B 树更小。如果把所有同一内部结点 的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就 越多。相对来说 IO 读写次数也就降低了。 + +- B+树的查询效率更加稳定 + + 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须 走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。 + +### MySQL中的B+Tree + +MySql索引数据结构对经典的B+Tree进行了优化。在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能。 + +MySQL中的 B+Tree 索引结构示意图: + +![1555906287178](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555906287178.png) + + + +### 聚簇索引和非聚簇索引 + +聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。术语‘聚簇’表示数据行和相邻的键值聚簇的存储 在一起。如下图,左侧的索引就是聚簇索引,因为数据行在磁盘的排列和索引排序保持一致。 + + + +![image-20210520211641159](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210520211641159.png) + +**聚簇索引的好处** + +按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不不用从多 个数据块中提取数据,所以节省了大量的 io 操作。 + +**聚簇索引的限制** + +对于 mysql 数据库目前只有 innodb 数据引擎支持聚簇索引,而 Myisam 并不支持聚簇索引。 + +由于数据物理存储排序方式只能有一种,所以每个 Mysql 的表只能有一个聚簇索引。一般情况下就是该表的主键。 + +为了充分利用聚簇索引的聚簇的特性,所以 innodb 表的主键列尽量选用有序的顺序 id,而不建议用无序的 id,比如 uuid 这种。 + +### 时间复杂度 + +同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。 + +时间复杂度是指执行算法所需要的计算工作量,用大 O 表示记为:O(…) + +![image-20210520211835491](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210520211835491.png) + +![image-20210520212305695](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210520212305695.png) + +## 索引分类 + +### 单值索引 + +概念:即一个索引只包含单个列,一个表可以有多个单列索引 + +- 所表一起创建 + + ```sql + CREATE TABLE customer ( + id INT (10) UNSIGNED AUTO_INCREMENT, + customer_no VARCHAR (200), + customer_name VARCHAR (200), + PRIMARY KEY (id), + KEY (customer_name) #this + ); + ``` + +- 单独建单值索引 + + ```sql + CREATE INDEX idx_customer_name + ON customer (customer_name); + ``` + +### 唯一索引 + +概念:索引列的值必须唯一,但允许有空值 + +- 随表一起创建 + + ```sql + CREATE TABLE customer ( + id INT (10) UNSIGNED AUTO_INCREMENT, + customer_no VARCHAR (200), + customer_name VARCHAR (200), + PRIMARY KEY (id), + KEY (customer_name), + UNIQUE (customer_no) #this + ); + ``` + +- 单独建唯一索引: + + ```sql + CREATE UNIQUE INDEX idx_customer_no + ON customer (customer_no); + ``` + + + +### 复合索引 + +概念:即一个索引包含多个列 + +- 随表一起建索引 + + ```sql + CREATE TABLE customer ( + id INT (10) UNSIGNED AUTO_INCREMENT, + customer_no VARCHAR (200), + customer_name VARCHAR (200), + PRIMARY KEY (id), + KEY (customer_name), + UNIQUE (customer_name), + KEY (customer_no, customer_name) #this + ); + ``` + +- 单独建索引: + + ```sql + CREATE INDEX idx_no_name + ON customer (customer_no, customer_name); + ``` + + + +### 主键索引 + +概念:设定为主键后数据库会自动建立索引,innodb为聚簇索引 + +- 随表一起建索引 + + ```sql + CREATE TABLE customer ( + id INT (10) UNSIGNED AUTO_INCREMENT, + customer_no VARCHAR (200), + customer_name VARCHAR (200), + PRIMARY KEY (id) #this + ); + ``` + +- 单独建主键索引: + + ```sql + ALTER TABLE customer ADD PRIMARY KEY customer(customer_no); + ``` + +- 删除主键索引 + + ```sql + ALTER TABLE customer drop PRIMARY KEY ; + ``` + +- 修改建主键索引 + + 必须先删除掉(drop)原索引,再新建(add)索引 + + + +### 索引基本语法 + +- 创建 + + ```sql + CREATE [UNIQUE] INDEX [indexName] + ON table_name(column)) + ``` + + 示例 : 为city表中的city_name字段创建索引 ; + + ![1551438009843](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1551438009843.png) + +- 删除 + + ```sql + DROP INDEX index_name ON tbl_name; + ``` + +- 查看 + + ```sql + show index from table_name; + ``` + +- AlTER + + ``` + 1). alter table tb_name add primary key(column_list); + + 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL + + 2). alter table tb_name add unique index_name(column_list); + + 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次) + + 3). alter table tb_name add index index_name(column_list); + + 添加普通索引, 索引值可以出现多次。 + + 4). alter table tb_name add fulltext index_name(column_list); + + 该语句指定了索引为FULLTEXT, 用于全文索引 + + ``` + + + + +## 索引设计原则 + + + +索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率,更高效的使用索引。 + +- 对查询频次较高,且数据量比较大的表建立索引。 + +- 索引字段的选择,最佳候选列应当从where子句的条件中提取,如果where子句中的组合比较多,那么应当挑选最常用、过滤效果最好的列的组合。 + +- 使用唯一索引,区分度越高,使用索引的效率越高。 + +- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价自然也就水涨船高。对于插入、更新、删除等DML操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低DML操作的效率,增加相应操作的时间消耗。另外索引过多的话,MySQL也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但无疑提高了选择的代价。 + +- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的I/O效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升MySQL访问索引的I/O效率。 + +- 利用最左前缀,N个列组合而成的组合索引,那么相当于是创建了N个索引,如果查询时where子句中使用了组成该索引的前几个字段,那么这条查询SQL可以利用组合索引来提升查询效率。 + + + + 创建复合索引 + + ```sql + CREATE INDEX idx_name_email_status ON tb_seller(NAME,email,STATUS); + ``` + + 就相当于 + + ​ 对 name 创建索引 ; + ​ 对 name , email 创建了索引 ; + ​ 对 name , email, status 创建了索引 ; + +**不适合创建索引的情况** + +- 表记录太少 +- 经常增删改的表或者字段 +- Where 条件里用不到的字段不创建索引 +- 过滤性不好的不适合建索引 + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/10.MySQL - \344\274\230\345\214\226SQL\346\243\200\346\265\213\346\255\245\351\252\244.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/10.MySQL - \344\274\230\345\214\226SQL\346\243\200\346\265\213\346\255\245\351\252\244.md" new file mode 100644 index 00000000..8093105d --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/10.MySQL - \344\274\230\345\214\226SQL\346\243\200\346\265\213\346\255\245\351\252\244.md" @@ -0,0 +1,590 @@ +--- +title: MySQL - 优化SQL检测步骤 +permalink: /mysql/optimize-sql-check/ +date: 2021-05-20 20:51:46 +--- + +# 优化SQL步骤 + + + + + +- [查看SQL执行频率](#%E6%9F%A5%E7%9C%8Bsql%E6%89%A7%E8%A1%8C%E9%A2%91%E7%8E%87) +- [定位低效率执行SQL](#%E5%AE%9A%E4%BD%8D%E4%BD%8E%E6%95%88%E7%8E%87%E6%89%A7%E8%A1%8Csql) +- [explain分析执行计划](#explain%E5%88%86%E6%9E%90%E6%89%A7%E8%A1%8C%E8%AE%A1%E5%88%92) + - [环境准备](#%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87) + - [explain 之 id](#explain-%E4%B9%8B-id) + - [explain 之 select_type](#explain-%E4%B9%8B-select_type) + - [explain 之 table](#explain-%E4%B9%8B-table) + - [explain 之 type](#explain-%E4%B9%8B-type) + - [explain 之 key](#explain-%E4%B9%8B-key) + - [explain 之 rows](#explain-%E4%B9%8B-rows) + - [explain 之 extra](#explain-%E4%B9%8B-extra) +- [show profile分析SQL](#show-profile%E5%88%86%E6%9E%90sql) +- [trace分析优化器执行计划](#trace%E5%88%86%E6%9E%90%E4%BC%98%E5%8C%96%E5%99%A8%E6%89%A7%E8%A1%8C%E8%AE%A1%E5%88%92) + + + + + +在应用的的开发过程中,由于初期数据量小,开发人员写 SQL 语句时更重视功能上的实现,但是当应用系统正式上线后,随着生产数据量的急剧增长,很多 SQL 语句开始逐渐显露出性能问题,对生产的影响也越来越大,此时这些有问题的 SQL 语句就成为整个系统性能的瓶颈,因此我们必须要对它们进行优化,本章将详细介绍在 MySQL 中优化 SQL 语句的方法。 + +当面对一个有 SQL 性能问题的数据库时,我们应该从何处入手来进行系统的分析,使得能够尽快定位问题 SQL 并尽快解决问题。 + + + +## 查看SQL执行频率 + +MySQL 客户端连接成功后,通过 `show [session|global] status` 命令可以提供服务器状态信息。 + +show [session|global] status 可以根据需要加上参数“session”或者“global”来显示 session 级(当前连接)的计结果和 global 级(自数据库上次启动至今)的统计结果。如果不写,默认使用参数是“session”。 + +下面的命令显示了当前 session 中所有统计参数的值: + +```sql +show status like 'Com_______'; +``` + +Com_xxx 表示每个 xxx 语句执行的次数 + +![1552487172501](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1552487172501.png) + +```sql +show status like 'Innodb_rows_%'; +``` + +我们通常比较关心的是以下几个统计参数 + +| 参数 | 含义 | +| :------------------- | ------------------------------------------------------------ | +| Com_select | 执行 select 操作的次数,一次查询只累加 1。 | +| Com_insert | 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次。 | +| Com_update | 执行 UPDATE 操作的次数。 | +| Com_delete | 执行 DELETE 操作的次数。 | +| Innodb_rows_read | select 查询返回的行数。 | +| Innodb_rows_inserted | 执行 INSERT 操作插入的行数。 | +| Innodb_rows_updated | 执行 UPDATE 操作更新的行数。 | +| Innodb_rows_deleted | 执行 DELETE 操作删除的行数。 | +| Connections | 试图连接 MySQL 服务器的次数。 | +| Uptime | 服务器工作时间。 | +| Slow_queries | 慢查询的次数。 | + +Com_*** : 这些参数对于所有存储引擎的表操作都会进行累计。 + +Innodb_*** : 这几个参数只是针对InnoDB 存储引擎的,累加的算法也略有不同。 + +## 定位低效率执行SQL + + + +- 慢查询日志 + + 通过慢查询日志定位那些执行效率较低的 SQL 语句,用 log-slow-queries[file_name] 选项启动时,mysqld 写一个包含所有执行时间超过 long_query_time 秒的 SQL 语句的日志文件。具体可以查看日志管理的相关部分。 + +- show processlist + + 慢查询日志在查询结束以后才纪录,所以在应用反映执行效率出现问题的时候查询慢查询日志并不能定位问题,可以使用 `show processlist` 命令查看当前MySQL在进行的线程,包括线程的状态、是否锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化。 + +![1556098544349](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556098544349.png) + +1) id列,用户登录mysql时,系统分配的"connection_id",可以使用函数connection_id()查看 + +2) user列,显示当前用户。如果不是root,这个命令就只显示用户权限范围的sql语句 + +3) host列,显示这个语句是从哪个ip的哪个端口上发的,可以用来跟踪出现问题语句的用户 + +4) db列,显示这个进程目前连接的是哪个数据库 + +5) command列,显示当前连接的执行的命令,一般取值为休眠(sleep),查询(query),连接(connect)等 + +6) time列,显示这个状态持续的时间,单位是秒 + +7) state列,显示使用当前连接的sql语句的状态,很重要的列。state描述的是语句执行中的某一个状态。一个sql语句,以查询为例,可能需要经过copying to tmp table、sorting result、sending data等状态才可以完成 + +8) info列,显示这个sql语句,是判断问题语句的一个重要依据 + +## explain分析执行计划 + +通过 EXPLAIN 或者 DESC命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。 + +```sql +explain select * from tb_item where id = 1; +``` + +![1552487489859](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1552487489859.png) + + + +![1552487526919](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1552487526919.png) + +| 字段 | 含义 | +| ------------- | ------------------------------------------------------------ | +| id | select查询的序列号,是一组数字,表示的是查询中执行select子句或者是操作表的顺序。 | +| select_type | 表示 SELECT 的类型,常见的取值有 SIMPLE(简单表,即不使用表连接或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION 中的第二个或者后面的查询语句)、SUBQUERY(子查询中的第一个 SELECT)等 | +| table | 输出结果集的表 | +| type | 表示表的连接类型,性能由好到差的连接类型为( system ---> const -----> eq_ref ------> ref -------> ref_or_null----> index_merge ---> index_subquery -----> range -----> index ------> all ) | +| possible_keys | 表示查询时,可能使用的索引 | +| key | 表示实际使用的索引 | +| key_len | 索引字段的长度 | +| rows | 扫描行的数量 | +| extra | 执行情况的说明和描述 | + +### 环境准备 + +![1556122799330](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556122799330.png) + +```sql +CREATE TABLE `t_role` ( + `id` VARCHAR(32) NOT NULL, + `role_name` VARCHAR(255) DEFAULT NULL, + `role_code` VARCHAR(255) DEFAULT NULL, + `description` VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_role_name` (`role_name`) +) ENGINE=INNODB DEFAULT CHARSET=utf8; + +CREATE TABLE `t_user` ( + `id` VARCHAR(32) NOT NULL, + `username` VARCHAR(45) NOT NULL, + `password` VARCHAR(96) NOT NULL, + `name` VARCHAR(45) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_user_username` (`username`) +) ENGINE=INNODB DEFAULT CHARSET=utf8; + +CREATE TABLE `user_role` ( + `id` INT(11) NOT NULL AUTO_INCREMENT , + `user_id` VARCHAR(32) DEFAULT NULL, + `role_id` VARCHAR(32) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fk_ur_user_id` (`user_id`), + KEY `fk_ur_role_id` (`role_id`), + CONSTRAINT `fk_ur_role_id` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT `fk_ur_user_id` FOREIGN KEY (`user_id`) REFERENCES `t_user` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION +) ENGINE=INNODB DEFAULT CHARSET=utf8; + +INSERT INTO `t_user` (`id`, `username`, `password`, `name`) VALUES('1','super','$2a$10$TJ4TmCdK.X4wv/tCqHW14.w70U3CC33CeVncD3SLmyMXMknstqKRe','超级管理员'); +INSERT INTO `t_user` (`id`, `username`, `password`, `name`) VALUES('2','admin','$2a$10$TJ4TmCdK.X4wv/tCqHW14.w70U3CC33CeVncD3SLmyMXMknstqKRe','系统管理员'); +INSERT INTO `t_user` (`id`, `username`, `password`, `name`) VALUES('3','itcast','$2a$10$8qmaHgUFUAmPR5pOuWhYWOr291WJYjHelUlYn07k5ELF8ZCrW0Cui','test02'); +INSERT INTO `t_user` (`id`, `username`, `password`, `name`) VALUES('4','stu1','$2a$10$pLtt2KDAFpwTWLjNsmTEi.oU1yOZyIn9XkziK/y/spH5rftCpUMZa','学生1'); +INSERT INTO `t_user` (`id`, `username`, `password`, `name`) VALUES('5','stu2','$2a$10$nxPKkYSez7uz2YQYUnwhR.z57km3yqKn3Hr/p1FR6ZKgc18u.Tvqm','学生2'); +INSERT INTO `t_user` (`id`, `username`, `password`, `name`) VALUES('6','t1','$2a$10$TJ4TmCdK.X4wv/tCqHW14.w70U3CC33CeVncD3SLmyMXMknstqKRe','老师1'); + +INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('5','学生','student','学生'); +INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('7','老师','teacher','老师'); +INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('8','教学管理员','teachmanager','教学管理员'); +INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('9','管理员','admin','管理员'); +INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('10','超级管理员','super','超级管理员'); + +INSERT INTO user_role(id,user_id,role_id) VALUES(NULL, '1', '5'),(NULL, '1', '7'),(NULL, '2', '8'),(NULL, '3', '9'),(NULL, '4', '8'),(NULL, '5', '10') ; + +``` + +### explain 之 id + +id 字段是 select查询的序列号,是一组数字,表示的是查询中执行select子句或者是操作表的顺序。id 情况有三种 + +1) id 相同表示加载表的顺序是从上到下。 + +```sql +explain select * from t_role r, t_user u, user_role ur where r.id = ur.role_id and u.id = ur.user_id ; +``` + +![1556102471304](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556102471304.png) + +2) id 不同id值越大,优先级越高,越先被执行。 + +```sql +EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1')) +``` + +![1556103009534](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556103009534.png) + +3) id 有相同,也有不同,同时存在。id相同的可以认为是一组,从上往下顺序执行;在所有的组中,id的值越大,优先级越高,越先执行。 + +```sql +EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ; +``` + +![1556103294182](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556103294182.png) + +### explain 之 select_type + +表示 SELECT 的类型,常见的取值,如下表所示: + +| select_type | 含义 | +| ------------ | ------------------------------------------------------------ | +| SIMPLE | 简单的select查询,查询中不包含子查询或者UNION | +| PRIMARY | 查询中若包含任何复杂的子查询,最外层查询标记为该标识 | +| SUBQUERY | 在SELECT 或 WHERE 列表中包含了子查询 | +| DERIVED | 在FROM 列表中包含的子查询,被标记为 DERIVED(衍生) MYSQL会递归执行这些子查询,把结果放在临时表中 | +| UNION | 若第二个SELECT出现在UNION之后,则标记为UNION ; 若UNION包含在FROM子句的子查询中,外层SELECT将被标记为 : DERIVED | +| UNION RESULT | 从UNION表获取结果的SELECT | + +- SIMPLE + + SIMPLE 代表单表查询 + + ```sql + EXPLAIN SELECT * FROM t_user; + ``` + + ![image-20210522183120093](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210522183120093.png) + + + +- PRIMARY、SUBQUERY + + 在 SELECT 或 WHERE 列表中包含了子查询。最外层查询则被标记为 Primary。 + + ```sql + explain select * from t_user where id = (select id from user_role where role_id='9' ); + ``` + + ![image-20210522183137975](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210522183137975.png) + +- DERIVED + + 在 FROM 列表中包含的子查询被标记为 DERIVED(衍生),MySQL 会递归执行这些子查询, 把结果放在临时表里。 + + ```sql + explain select a.* from (select * from t_user where id in('1','2') ) a; + ``` + + mysql 5.7 中为 `simple` + + ![image-20210522180244911](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210522180244911.png) + + + + mysql 5.6 中: + + ![image-20210522182540547](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210522182540547.png) + + + +- union + + ```sql + explain select * from t_user where id='1' union select * from t_user where id='2'; + ``` + + ![image-20210522183718254](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210522183718254.png) + + + +### explain 之 table + +展示这一行的数据是关于哪一张表的 + +没有与之关系的表为 NULL + +![image-20210522190653729](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210522190653729.png) + + + +### explain 之 type + +type 显示的是访问类型,是较为重要的一个指标,可取值为: + +| type | 含义 | +| ------ | ------------------------------------------------------------ | +| NULL | MySQL不访问任何表,索引,直接返回结果 | +| system | 表只有一行记录(等于系统表),这是const类型的特例,一般不会出现 | +| const | 表示通过索引一次就找到了,const 用于比较primary key 或者 unique 索引。因为只匹配一行数据,所以很快。如将主键置于where列表中,MySQL 就能将该查询转换为一个常亮。const于将 "主键" 或 "唯一" 索引的所有部分与常量值进行比较 | +| eq_ref | 类似ref,区别在于使用的是唯一索引,使用主键的关联查询,关联查询出的记录只有一条。常见于主键或唯一索引扫描 | +| ref | 非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,返回所有匹配某个单独值的所有行(多个) | +| range | 只检索给定返回的行,使用一个索引来选择行。 where 之后出现 between , < , > , in 等操作。 | +| index | index 与 ALL的区别为 index 类型只是遍历了索引树, 通常比ALL 快, ALL 是遍历数据文件。 | +| all | 将遍历全表以找到匹配的行 | + +结果值从最好到最坏以此是: + +- NULL > system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL + +- system > const > eq_ref > ref > range > index > ALL + +一般来说, 我们需要保证查询至少达到 range 级别, 最好达到ref + +### explain 之 key + + + +- possible_keys : + + 显示可能应用在这张表的索引, 一个或多个。 + +- key + + 实际使用的索引, 如果为NULL, 则没有使用索引。 + +- key_len + + 表示索引中使用的字节数, 该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下, 长度越短越好 。 + +### explain 之 rows + +扫描行的数量。 + +### explain 之 extra + +其他的额外的执行计划信息,在该列展示 。 + +| extra | 含义 | +| ---------------- | ------------------------------------------------------------ | +| using filesort | 说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取, 称为 “文件排序”, 效率低。 | +| using temporary | 使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于 order by 和 group by; 效率低 | +| using index | 表示相应的select操作使用了覆盖索引, 避免访问表的数据行, 效率不错。 | + +## show profile分析SQL + +Mysql从5.0.37版本开始增加了对 show profiles 和 show profile 语句的支持。 + +show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。 + +通过 have_profiling 参数,能够看到当前MySQL是否支持profile: + +![1552488401999](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1552488401999.png) + +默认profiling是关闭的,可以通过set语句在Session级别开启profiling: + +![1552488372405](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1552488372405.png) + +```sql +set profiling=1; //开启profiling 开关; +``` + +通过profile,我们能够更清楚地了解SQL执行的过程。 + +我们可以执行一系列的操作: + +```sql +show databases; + +use db01; + +show tables; + +select * from tb_item where id < 5; + +select count(*) from tb_item; +``` + +执行完上述命令之后,再执行 `show profiles` 指令, 来查看SQL语句执行的耗时: + +![1552489017940](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1552489017940.png) + +通过 `show profile for query query_id` 语句查看该SQL执行过程中每个线程的状态和消耗的时间 + +![1552489053763](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1552489053763.png) + +TIP:Sending data 状态表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅是返回个客户端。由于在 Sending data 状态下,MySQL 线程往往需要做大量的磁盘读取操作,所以经常是整各查询中耗时最长的状态。 + +在获取到最消耗时间的线程状态后,MySQL支持进一步选择all、cpu、block io 、context switch、page faults等明细类型类查看MySQL在使用什么资源上耗费了过高的时间。例如,选择查看CPU的耗费时间 : + +![1552489671119](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1552489671119.png) + +| 字段 | 含义 | +| ---------- | ------------------------------ | +| Status | sql 语句执行的状态 | +| Duration | sql 执行过程中每一个步骤的耗时 | +| CPU_user | 当前用户占有的cpu | +| CPU_system | 系统占有的cpu | + +## trace分析优化器执行计划 + +MySQL5.6提供了对SQL的跟踪trace, 通过trace文件能够进一步了解为什么优化器选择A计划, 而不是选择B计划。 + +打开trace , 设置格式为 JSON,并设置trace最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示。 + +```sql +SET optimizer_trace="enabled=on",end_markers_in_json=on; +set optimizer_trace_max_mem_size=1000000; +``` + +执行SQL语句 : + +```sql +select * from tb_item where id < 4; +``` + +最后, 检查 information_schema.optimizer_trace 就可以知道MySQL是如何执行SQL的 : + +```sql +select * from information_schema.optimizer_trace\G; +``` + +```json +*************************** 1. row *************************** +QUERY: select * from tb_item where id < 4 +TRACE: { + "steps": [ + { + "join_preparation": { + "select#": 1, + "steps": [ + { + "expanded_query": "/* select#1 */ select `tb_item`.`id` AS `id`,`tb_item`.`title` AS `title`,`tb_item`.`price` AS `price`,`tb_item`.`num` AS `num`,`tb_item`.`categoryid` AS `categoryid`,`tb_item`.`status` AS `status`,`tb_item`.`sellerid` AS `sellerid`,`tb_item`.`createtime` AS `createtime`,`tb_item`.`updatetime` AS `updatetime` from `tb_item` where (`tb_item`.`id` < 4)" + } + ] /* steps */ + } /* join_preparation */ + }, + { + "join_optimization": { + "select#": 1, + "steps": [ + { + "condition_processing": { + "condition": "WHERE", + "original_condition": "(`tb_item`.`id` < 4)", + "steps": [ + { + "transformation": "equality_propagation", + "resulting_condition": "(`tb_item`.`id` < 4)" + }, + { + "transformation": "constant_propagation", + "resulting_condition": "(`tb_item`.`id` < 4)" + }, + { + "transformation": "trivial_condition_removal", + "resulting_condition": "(`tb_item`.`id` < 4)" + } + ] /* steps */ + } /* condition_processing */ + }, + { + "table_dependencies": [ + { + "table": "`tb_item`", + "row_may_be_null": false, + "map_bit": 0, + "depends_on_map_bits": [ + ] /* depends_on_map_bits */ + } + ] /* table_dependencies */ + }, + { + "ref_optimizer_key_uses": [ + ] /* ref_optimizer_key_uses */ + }, + { + "rows_estimation": [ + { + "table": "`tb_item`", + "range_analysis": { + "table_scan": { + "rows": 9816098, + "cost": 2.04e6 + } /* table_scan */, + "potential_range_indices": [ + { + "index": "PRIMARY", + "usable": true, + "key_parts": [ + "id" + ] /* key_parts */ + } + ] /* potential_range_indices */, + "setup_range_conditions": [ + ] /* setup_range_conditions */, + "group_index_range": { + "chosen": false, + "cause": "not_group_by_or_distinct" + } /* group_index_range */, + "analyzing_range_alternatives": { + "range_scan_alternatives": [ + { + "index": "PRIMARY", + "ranges": [ + "id < 4" + ] /* ranges */, + "index_dives_for_eq_ranges": true, + "rowid_ordered": true, + "using_mrr": false, + "index_only": false, + "rows": 3, + "cost": 1.6154, + "chosen": true + } + ] /* range_scan_alternatives */, + "analyzing_roworder_intersect": { + "usable": false, + "cause": "too_few_roworder_scans" + } /* analyzing_roworder_intersect */ + } /* analyzing_range_alternatives */, + "chosen_range_access_summary": { + "range_access_plan": { + "type": "range_scan", + "index": "PRIMARY", + "rows": 3, + "ranges": [ + "id < 4" + ] /* ranges */ + } /* range_access_plan */, + "rows_for_plan": 3, + "cost_for_plan": 1.6154, + "chosen": true + } /* chosen_range_access_summary */ + } /* range_analysis */ + } + ] /* rows_estimation */ + }, + { + "considered_execution_plans": [ + { + "plan_prefix": [ + ] /* plan_prefix */, + "table": "`tb_item`", + "best_access_path": { + "considered_access_paths": [ + { + "access_type": "range", + "rows": 3, + "cost": 2.2154, + "chosen": true + } + ] /* considered_access_paths */ + } /* best_access_path */, + "cost_for_plan": 2.2154, + "rows_for_plan": 3, + "chosen": true + } + ] /* considered_execution_plans */ + }, + { + "attaching_conditions_to_tables": { + "original_condition": "(`tb_item`.`id` < 4)", + "attached_conditions_computation": [ + ] /* attached_conditions_computation */, + "attached_conditions_summary": [ + { + "table": "`tb_item`", + "attached": "(`tb_item`.`id` < 4)" + } + ] /* attached_conditions_summary */ + } /* attaching_conditions_to_tables */ + }, + { + "refine_plan": [ + { + "table": "`tb_item`", + "access_type": "range" + } + ] /* refine_plan */ + } + ] /* steps */ + } /* join_optimization */ + }, + { + "join_execution": { + "select#": 1, + "steps": [ + ] /* steps */ + } /* join_execution */ + } + ] /* steps */ +} +``` + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/12.MySQL - \347\264\242\345\274\225\347\232\204\344\275\277\347\224\250.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/12.MySQL - \347\264\242\345\274\225\347\232\204\344\275\277\347\224\250.md" new file mode 100644 index 00000000..37c2cc4f --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/12.MySQL - \347\264\242\345\274\225\347\232\204\344\275\277\347\224\250.md" @@ -0,0 +1,349 @@ +--- +title: MySQL - 索引的使用 +permalink: /mysql/index-use/ +date: 2021-05-20 20:51:46 +--- + + + + + +- [索引的使用](#%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BD%BF%E7%94%A8) + - [验证索引提升查询效率](#%E9%AA%8C%E8%AF%81%E7%B4%A2%E5%BC%95%E6%8F%90%E5%8D%87%E6%9F%A5%E8%AF%A2%E6%95%88%E7%8E%87) + - [准备环境](#%E5%87%86%E5%A4%87%E7%8E%AF%E5%A2%83) + - [避免索引失效](#%E9%81%BF%E5%85%8D%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88) + - [查看索引使用情况](#%E6%9F%A5%E7%9C%8B%E7%B4%A2%E5%BC%95%E4%BD%BF%E7%94%A8%E6%83%85%E5%86%B5) + - [练习](#%E7%BB%83%E4%B9%A0) + + + + + +# 索引的使用 + + + +索引是数据库优化最常用也是最重要的手段之一, 通过索引通常可以帮助用户解决大多数的MySQL的性能优化问题。 + + + +## 验证索引提升查询效率 + +在我们准备的表结构 tb_item 中, 一共存储了 300 万记录; + +1). 根据ID查询 + +```sql +select * from tb_item where id = 1999\G; +``` + +![1553261992653](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553261992653.png) + +查询速度很快, 接近0s , 主要的原因是因为id为主键, 有索引; + +![1553262044466](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553262044466.png) + + + +2). 根据 title 进行精确查询 + +```sql +select * from tb_item where title = 'iphoneX 移动3G 32G941'\G; +``` + +![1553262215900](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553262215900.png) + +查看SQL语句的执行计划 : + +![1553262469785](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553262469785.png) + + + +处理方案 , 针对title字段, 创建索引 : + +```sql +create index idx_item_title on tb_item(title); +``` + +![1553263229523](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553263229523.png) + + + +索引创建完成之后,再次进行查询 : + +![1553263302706](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553263302706.png) + +通过 explain , 查看执行计划,执行SQL时使用了刚才创建的索引 + +![1553263355262](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553263355262.png) + + + +## 准备环境 + +```sql +create table `tb_seller` ( + `sellerid` varchar (100), + `name` varchar (100), + `nickname` varchar (50), + `password` varchar (60), + `status` varchar (1), + `address` varchar (100), + `createtime` datetime, + primary key(`sellerid`) +)engine=innodb default charset=utf8mb4; + +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('alibaba','阿里巴巴','阿里小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('baidu','百度科技有限公司','百度小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('huawei','华为科技有限公司','华为小店','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('itcast','传智播客教育科技有限公司','传智播客','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('itheima','黑马程序员','黑马程序员','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('luoji','罗技科技有限公司','罗技小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('oppo','OPPO科技有限公司','OPPO官方旗舰店','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('ourpalm','掌趣科技股份有限公司','掌趣小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('qiandu','千度科技','千度小店','e10adc3949ba59abbe56e057f20f883e','2','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('sina','新浪科技有限公司','新浪官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); +insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('yijia','宜家家居','宜家家居旗舰店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); + + +create index idx_seller_name_sta_addr on tb_seller(name,status,address); +``` + +## 避免索引失效 + +**1). 全值匹配 ,对索引中所有列都指定具体值。** + +该情况下,索引生效,执行效率高。 + +```sql +explain select * from tb_seller where name='小米科技' and status='1' and address='北京市'\G; +``` + +![1556170997921](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556170997921.png) + +**2). 最左前缀法则** + +如果索引了多列(复合索引),要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。 + + + +匹配最左前缀法则,走索引: + +![1556171348995](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556171348995.png) + +违法最左前缀法则 , 索引失效: + +![1556171428140](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556171428140.png) + +如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: + +![1556171662203](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556171662203.png) + + + +**3). 范围查询,不能使用索引 。** + +![1556172256791](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556172256791.png) + +根据前面的两个字段name , status 查询是走索引的, 但是最后一个条件address 没有用到索引。 + + + +**4). 不要在索引列上进行运算操作,否则索引将失效。** + +![1556172813715](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556172813715.png) + +**5). 字符串不加单引号,造成索引失效。** + +![1556172967493](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556172967493.png) + +在查询时,没有对字符串加单引号,MySQL的查询优化器,会自动的进行类型转换,造成索引失效。 + +**6). 尽量使用覆盖索引,避免select *** + +尽量使用覆盖索引(只访问索引的查询(索引列完全包含查询列)),减少select * 。 + +![1556173928299](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556173928299.png) + +如果查询列,超出索引列,也会降低性能。 + +![1556173986068](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556173986068.png) + +**Extra:** + +- using index + + 使用覆盖索引的时候就会出现 + +- using where + + 在查找使用索引的情况下,需要回表去查询所需的数据 + +- using index condition + + 查找使用了索引,但是需要回表查询数据 + +- using index ; using where + + 查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据 + + + +**7). 用or分割开的条件, 如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。** + +示例,name字段是索引列 , 而createtime不是索引列,中间是or进行连接是不走索引的 : + +```sql +explain select * from tb_seller where name='黑马程序员' or createtime = '2088-01-01 12:00:00'\G; +``` + +![1556174994440](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556174994440.png) + + + +**8). 以%开头的Like模糊查询,索引失效。** + +如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。 + +```sql +explain select * from tb_seller where name like "%黑马程序员"; +``` + +![1556175114369](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556175114369.png) + +解决方案 : + +通过覆盖索引来解决 (不用 select *) + +![1556247686483](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556247686483.png) + +**9). 如果MySQL评估使用索引比全表更慢,则不使用索引。** + +我们先给 address 创建索引 + +```sql +create index idx_seller_address on tb_seller(address); +``` + +在我们表 tb_seller 中,12条地区数据其中11个是北京市 + +查北京地区的走全表扫描 + +![1556175445210](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556175445210.png) + + + +使用覆盖查询会走索引 + +```sql + explain select address from tb_seller where address='北京市'; +``` + +**10). is NULL , is NOT NULL 有时索引失效。** + +和上一条(9)差不多。 + +![1556180634889](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556180634889.png) + +**11). in 走索引, not in 索引失效。** + +在mysql 5.6中 + +![1556249602732](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556249602732.png) + + + +个人理解:not in 判断不存在的,需要对表进行大部分数据扫描,类似于第九条 + + + +mysql 5.7中都不失效: + +![image-20210523131109106](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210523131109106.png) + +**12). 单列索引和复合索引。** + +尽量使用复合索引,而少使用单列索引 。 + +创建复合索引 + +```sql +create index idx_name_sta_address on tb_seller(name, status, address); + +--就相当于创建了三个索引 : +-- name +-- name + status +-- name + status + address +``` + + + +创建单列索引 + +```sql +create index idx_seller_name on tb_seller(name); +create index idx_seller_status on tb_seller(status); +create index idx_seller_address on tb_seller(address); +``` + +数据库会选择一个最优的索引(辨识度最高索引)来使用,并不会使用全部索引 。 + +## 查看索引使用情况 + +```sql +show status like 'Handler_read%'; --当前会话级别 + +show global status like 'Handler_read%'; --全局级别 +``` + +![1552885364563](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1552885364563.png) + +- Handler_read_first + + 索引中第一条被读的次数。如果较高,表示服务器正执行大量全索引扫描(这个值越低越好)。 + +- Handler_read_key + + 如果索引正在工作,这个值代表一个行被索引值读的次数,如果值越低,表示索引得到的性能改善不高,因为索引不经常使用(这个值越高越好)。 + +- Handler_read_next + + 按照键顺序读下一行的请求数。如果你用范围约束或如果执行索引扫描来查询索引列,该值增加。 + +- Handler_read_prev + + 按照键顺序读前一行的请求数。该读方法主要用于优化ORDER BY ... DESC。 + +- Handler_read_rnd + + 根据固定位置读一行的请求数。如果你正执行大量查询并需要对结果进行排序该值较高。你可能使用了大量需要MySQL扫描整个表的查询或你的连接没有正确使用键。这个值较高,意味着运行效率低,应该建立索引来补救。 + +- Handler_read_rnd_next + + 在数据文件中读下一行的请求数。如果你正进行大量的表扫描,该值较高。通常说明你的表索引不正确或写入的查询没有利用索引。 + + + +## 练习 + +假设 index(a,b,c); + +| Where 语句 | 索引是否被使用 | +| ------------------------------------------------------- | ------------------------------------------ | +| where a = 3 | Y,使用到 a | +| where a = 3 and b = 5 | Y,使用到 a,b | +| where a = 3 and b = 5 and c = 4 | Y,使用到 a,b,c | +| where b = 3 或者 where b = 3 and c = 4 或者 where c = 4 | N(第二条,左前缀法则) | +| where a = 3 and c = 5 | 使用到 a, 但是 c 不可以,b 中间断了 | +| where a = 3 and b > 4 and c = 5 | 使用到 a 和 b, c 不能用在范围之后,b 断了 | +| where a is null and b is not null | is null 支持索引 但是 is not null 不支持 | +| where a <> 3 | 不能使用索引 | +| where abs(a) =3 | 不能使用索引 | +| where a = 3 and b like 'kk%' and c = 4 | Y,使用到 a,b,c | +| where a = 3 and b like '%kk' and c = 4 | Y,只用到 a | +| where a = 3 and b like '%kk%' and c = 4 | Y,只用到 a | +| where a = 3 and b like 'k%kk%' and c = 4 | Y,使用到 a,b,c | + + + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/14.MySQL - SQL\344\274\230\345\214\226.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/14.MySQL - SQL\344\274\230\345\214\226.md" new file mode 100644 index 00000000..30edc08f --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/14.MySQL - SQL\344\274\230\345\214\226.md" @@ -0,0 +1,431 @@ +--- +title: MySQL - SQL语句优化 +permalink: /mysql/sql-optimize/ +date: 2021-05-23 14:08:28 +--- + + + + + +- [大批量插入数据时](#%E5%A4%A7%E6%89%B9%E9%87%8F%E6%8F%92%E5%85%A5%E6%95%B0%E6%8D%AE%E6%97%B6) +- [优化insert语句](#%E4%BC%98%E5%8C%96insert%E8%AF%AD%E5%8F%A5) +- [优化order by语句](#%E4%BC%98%E5%8C%96order-by%E8%AF%AD%E5%8F%A5) + - [环境准备](#%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87) + - [两种排序方式](#%E4%B8%A4%E7%A7%8D%E6%8E%92%E5%BA%8F%E6%96%B9%E5%BC%8F) + - [Filesort 的优化](#filesort-%E7%9A%84%E4%BC%98%E5%8C%96) +- [优化group by 语句](#%E4%BC%98%E5%8C%96group-by-%E8%AF%AD%E5%8F%A5) +- [优化嵌套查询](#%E4%BC%98%E5%8C%96%E5%B5%8C%E5%A5%97%E6%9F%A5%E8%AF%A2) +- [优化OR条件](#%E4%BC%98%E5%8C%96or%E6%9D%A1%E4%BB%B6) +- [优化分页查询](#%E4%BC%98%E5%8C%96%E5%88%86%E9%A1%B5%E6%9F%A5%E8%AF%A2) +- [使用SQL提示](#%E4%BD%BF%E7%94%A8sql%E6%8F%90%E7%A4%BA) + + + +## 大批量插入数据时 + +环境准备: + +```sql +CREATE TABLE `tb_user_2` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(45) NOT NULL, + `password` varchar(96) NOT NULL, + `name` varchar(45) NOT NULL, + `birthday` datetime DEFAULT NULL, + `sex` char(1) DEFAULT NULL, + `email` varchar(45) DEFAULT NULL, + `phone` varchar(45) DEFAULT NULL, + `qq` varchar(32) DEFAULT NULL, + `status` varchar(32) NOT NULL COMMENT '用户状态', + `create_time` datetime NOT NULL, + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_user_username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; +``` + +当使用 `load` 命令导入数据的时候,适当的设置可以提高导入的效率。 + +![1556269346488](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556269346488.png) + +对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: + +**1) 主键顺序插入** + +因为InnoDB类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率。如果InnoDB表没有主键,那么系统会自动默认创建一个内部列作为主键,所以如果可以给表创建一个主键,将可以利用这点,来提高导入数据的效率。 + +> 脚本文件介绍 : +> +> ​ sql1.log ----> 主键有序 +> +> ​ sql2.log ----> 主键无序 + + + +插入ID顺序排列数据: + +![1555771750567](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555771750567.png) + +插入ID无序排列数据: + +![1555771959734](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555771959734.png) + + + +**2) 关闭唯一性校验** + +在导入数据前执行 SET UNIQUE_CHECKS=0,关闭唯一性校验 + +在导入结束后执行SET UNIQUE_CHECKS=1,恢复唯一性校验,可以提高导入的效率。 + +![1555772132736](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555772132736.png) + + + +**3) 手动提交事务** + +建议在导入前执行 SET AUTOCOMMIT=0,关闭自动提交 + +导入结束后再执行 SET AUTOCOMMIT=1,打开自动提交,也可以提高导入的效率。 + +![1555772351208](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555772351208.png) + + + +> load执行可能会报错 + +The used command is not allowed with this MySQL version + +错误的原因是没有开启 local_infile 模块。 + +**解决方法:** + +首先看一下 local_infile 模块是否打开: + +```msyql +show global variables like 'local_infile'; +``` + +显示如下: + +![image](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/20190406131719389.png) + +然后可以发现这个模块已经启用了: + +![image](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/20190406131859826.png) + + + +之后重启一下Mysql服务即可 + +## 优化insert语句 + + + +1.)如果需要同时对一张表插入很多行数据时,应该尽量使用多个值表的insert语句,这种方式将大大的缩减客户端与数据库之间的连接、关闭等消耗。使得效率比分开执行的单个insert语句快。 + +- 原始方式为: + + ```sql + insert into tb_test values(1,'Tom'); + insert into tb_test values(2,'Cat'); + insert into tb_test values(3,'Jerry'); + ``` + + 优化后的方案为 : + + ```sql + insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry'); + ``` + + +2.)在事务中进行数据插入。 + +```sql +start transaction; +insert into tb_test values(1,'Tom'); +insert into tb_test values(2,'Cat'); +insert into tb_test values(3,'Jerry'); +commit; +``` + +3.)数据有序插入 + +- 原 + + ```sql + insert into tb_test values(4,'Tim'); + insert into tb_test values(1,'Tom'); + insert into tb_test values(3,'Jerry'); + insert into tb_test values(5,'Rose'); + insert into tb_test values(2,'Cat'); + ``` + +- 优化后 + + ```sql + insert into tb_test values(1,'Tom'); + insert into tb_test values(2,'Cat'); + insert into tb_test values(3,'Jerry'); + insert into tb_test values(4,'Tim'); + insert into tb_test values(5,'Rose'); + ``` + + + +## 优化order by语句 + +### 环境准备 + +```SQL +CREATE TABLE `emp` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `age` int(3) NOT NULL, + `salary` int(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +insert into `emp` (`id`, `name`, `age`, `salary`) values('1','Tom','25','2300'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('2','Jerry','30','3500'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('3','Luci','25','2800'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('4','Jay','36','3500'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('5','Tom2','21','2200'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('6','Jerry2','31','3300'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('7','Luci2','26','2700'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('8','Jay2','33','3500'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('9','Tom3','23','2400'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('10','Jerry3','32','3100'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('11','Luci3','26','2900'); +insert into `emp` (`id`, `name`, `age`, `salary`) values('12','Jay3','37','4500'); + +create index idx_emp_age_salary on emp(age,salary); +``` + +### 两种排序方式 + +1). 第一种是通过对返回数据进行排序,也就是通常说的 filesort 排序 + +不是通过索引直接返回排序结果的排序都叫 FileSort 排序。 + +![1556335817763](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556335817763.png) + +2). 第二种通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高。 + +![1556335866539](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556335866539.png) + +多字段排序 + +![1556336352061](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556336352061.png) + + + +了解了MySQL的排序方式,优化目标就清晰了:尽量减少额外的排序,通过索引直接返回有序数据。where 条件和Order by 使用相同的索引,并且Order By 的顺序和索引顺序相同, 并且Order by 的字段都是升序,或者都是降序。否则肯定需要额外的操作,这样就会出现 FileSort。 + + + +### Filesort 的优化 + +通过创建合适的索引,能够减少 Filesort 的出现,但是在某些情况下,条件限制不能让 Filesort 消失,那就需要加快 Filesort的排序操作。对于Filesort ,MySQL 有两种排序算法: + +1) 两次扫描算法 :MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果sort buffer不够,则在临时表 temporary table 中存储排序结果。完成排序之后,再根据行指针回表读取记录,该操作可能会导致大量随机I/O操作。 + +2)一次扫描算法:一次性取出满足条件的所有字段,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法要高。 + + + +MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段总大小, 来判定是否那种排序算法,如果 max_length_for_sort_data 更大,那么使用第二种优化之后的算法;否则使用第一种。 + +可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量,来增大排序区的大小,提高排序的效率。 + +![1556338367593](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556338367593.png) + + + +## 优化group by 语句 + +由于 GROUP BY 实际上也同样会进行排序操作,而且与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。所以,在GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引。 + +如果查询包含 group by 但是想要避免排序结果的消耗, 则可以执行order by null 禁止排序。如下 : + +```sql +drop index idx_emp_age_salary on emp; -- 删除之前创建的索引 + +explain select age,count(*) from emp group by age; +``` + +![1556339573979](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556339573979.png) + +优化后 + +```sql +explain select age,count(*) from emp group by age order by null; +``` + +![1556339633161](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556339633161.png) + +从上面的例子可以看出,第一个SQL语句需要进行"filesort",而第二个SQL由于 `order by null` 不需要进行 "filesort", 而上文提过 Filesort 往往非常耗费时间。 + + + +创建索引 : + +```SQL +create index idx_emp_age_salary on emp(age,salary); +``` + +![1556339688158](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556339688158.png) + + + +**但是在 mysql 5.7 中:** + +![image-20210523150730770](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210523150730770.png) + + + +## 优化嵌套查询 + +Mysql4.1版本之后,开始支持SQL的子查询。这个技术可以使用SELECT语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,子查询是可以被更高效的连接(JOIN)替代。 + +示例 ,查找有角色的所有的用户信息 : + +```SQL + explain select * from t_user where id in (select user_id from user_role ); +``` + +执行计划为 : + +![1556359399199](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556359399199.png) + +优化后 : + +```SQL +explain select * from t_user u , user_role ur where u.id = ur.user_id; +``` + +![1556359482142](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556359482142.png) + +连接(Join)查询之所以更有效率一些 ,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上需要两个步骤的查询工作。 + +在 mysql 5.7 中 + +![image-20210523151026166](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/image-20210523151026166.png) + + + +## 优化OR条件 + +对于包含OR的查询子句,如果要利用索引,则OR之间的每个条件列都必须用到索引 , 而且不能使用到复合索引; 如果没有索引,则应该考虑增加索引。 + +获取 emp 表中的所有的索引 : + +![1556354464657](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556354464657.png) + +示例 : + +```SQL +explain select * from emp where id = 1 or age = 30; +``` + +![1556354887509](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556354887509.png) + +![1556354920964](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556354920964.png) + +建议使用 union 替换 or : + +![1556355027728](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556355027728.png) + +我们来比较下重要指标,发现主要差别是 type 和 ref 这两项 + +type 显示的是访问类型,是较为重要的一个指标,结果值从好到坏依次是: + +``` +system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL +``` + +UNION 语句的 type 值为 ref,OR 语句的 type 值为 range,可以看到这是一个很明显的差距 + +UNION 语句的 ref 值为 const,OR 语句的 type 值为 null,const 表示是常量值引用,非常快 + +这两项的差距就说明了 UNION 要优于 OR 。 + +在 mysql 8.0 中,默认优化了,具体自行测试。 + + + +## 优化分页查询 + +一般分页查询时,通过创建覆盖索引能够比较好地提高性能。一个常见又非常头疼的问题就是 limit 2000000,10 ,此时需要MySQL排序前2000010 记录,仅仅返回2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大 。 + +![1556361314783](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556361314783.png) + +**优化思路一** + +在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。 + +![1556416102800](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556416102800.png) + + + +**优化思路二** + +该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 。 + +![1556363928151](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556363928151.png) + + + +## 使用SQL提示 + +SQL提示,是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。 + + + +**USE INDEX** + +在查询语句中表名的后面,添加 use index 来提供希望MySQL去参考的索引列表,就可以让MySQL不再考虑其他可用的索引。 + +``` +create index idx_seller_name on tb_seller(name); +``` + +![1556370971576](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556370971576.png) + + + +**IGNORE INDEX** + +如果用户只是单纯的想让MySQL忽略一个或者多个索引,则可以使用 ignore index 作为 hint 。 + +``` + explain select * from tb_seller ignore index(idx_seller_name) where name = '小米科技'; +``` + +![1556371004594](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556371004594.png) + + + +**FORCE INDEX** + +为强制MySQL使用一个特定的索引,可在查询中使用 force index 作为hint 。 + +``` SQL +create index idx_seller_address on tb_seller(address); +``` + +![1556371355788](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556371355788.png) + + + + + + + + + + + + + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/16.MySQL - \347\274\223\345\255\230\346\237\245\350\257\242.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/16.MySQL - \347\274\223\345\255\230\346\237\245\350\257\242.md" new file mode 100644 index 00000000..6453e72a --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/16.MySQL - \347\274\223\345\255\230\346\237\245\350\257\242.md" @@ -0,0 +1,163 @@ +--- +title: MySQL - 缓存查询 +permalink: /mysql/cache-query/ +date: 2021-05-24 15:10:03 +--- + + + + + +- [概述](#%E6%A6%82%E8%BF%B0) +- [查询缓存配置](#%E6%9F%A5%E8%AF%A2%E7%BC%93%E5%AD%98%E9%85%8D%E7%BD%AE) +- [开启查询缓存](#%E5%BC%80%E5%90%AF%E6%9F%A5%E8%AF%A2%E7%BC%93%E5%AD%98) +- [查询缓存SELECT选项](#%E6%9F%A5%E8%AF%A2%E7%BC%93%E5%AD%98select%E9%80%89%E9%A1%B9) +- [查询缓存失效的情况](#%E6%9F%A5%E8%AF%A2%E7%BC%93%E5%AD%98%E5%A4%B1%E6%95%88%E7%9A%84%E6%83%85%E5%86%B5) + + + +## 概述 + +开启Mysql的查询缓存,当执行完全相同的SQL语句的时候,服务器就会直接从缓存中读取结果,当数据被修改,之前的缓存会失效,修改比较频繁的表不适合做查询缓存。 + + + + ![20180919131632347](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/20180919131632347.png) + +1. 客户端发送一条查询给服务器; +2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果。否则进入下一阶段; +3. 服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划; +4. MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询; +5. 将结果返回给客户端。 + +## 查询缓存配置 + +- 查看当前的MySQL数据库是否支持查询缓存: + +```SQL +SHOW VARIABLES LIKE 'have_query_cache'; +``` + +![1555249929012](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555249929012.png) + +在Mysql8,已经取消了查询缓存 + + + +- 查看当前MySQL是否开启了查询缓存 : + +```SQL +SHOW VARIABLES LIKE 'query_cache_type'; +``` + +![1555250015377](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555250015377.png) + +- 查看查询缓存的占用大小 : + +```SQL +SHOW VARIABLES LIKE 'query_cache_size'; +``` + +![1555250142451](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555250142451.png) + +- 查看查询缓存的状态变量: + +```SQL +SHOW STATUS LIKE 'Qcache%'; +``` + +![1555250443958](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555250443958.png) + +各个变量的含义如下: + +| 参数 | 含义 | +| ----------------------- | ------------------------------------------------------------ | +| Qcache_free_blocks | 查询缓存中的可用内存块数 | +| Qcache_free_memory | 查询缓存的可用内存量 | +| Qcache_hits | 查询缓存命中数 | +| Qcache_inserts | 添加到查询缓存的查询数 | +| Qcache_lowmen_prunes | 由于内存不足而从查询缓存中删除的查询数 | +| Qcache_not_cached | 非缓存查询的数量(由于 query_cache_type 设置而无法缓存或未缓存) | +| Qcache_queries_in_cache | 查询缓存中注册的查询数 | +| Qcache_total_blocks | 查询缓存中的块总数 | + + + +## 开启查询缓存 + +MySQL的查询缓存默认是关闭的,需要手动配置参数 query_cache_type , 来开启查询缓存。 + +query_cache_type 该参数的可取值有三个 : + +| 值 | 含义 | +| ----------- | ------------------------------------------------------------ | +| OFF 或 0 | 查询缓存功能关闭 | +| ON 或 1 | 查询缓存功能打开,SELECT的结果符合缓存条件即会缓存,否则,不予缓存,显式指定 SQL_NO_CACHE,不予缓存 | +| DEMAND 或 2 | 查询缓存功能按需进行,显式指定 SQL_CACHE 的SELECT语句才会缓存;其它均不予缓存 | + +在 `/usr/my.cnf` 配置中(宝塔在 `/etc/my.cnf` ),增加以下配置 : + +```properties +# 开启mysql的查询缓存 +query_cache_type=1 +``` + +配置完毕之后,重启服务既可生效 , `service mysqld restart `; + +然后就可以在命令行执行SQL语句进行验证 ,执行一条比较耗时的SQL语句,然后再多执行几次,查看后面几次的执行时间;获取通过查看查询缓存的缓存命中数,来判定是否走查询缓存。 + +## 查询缓存SELECT选项 + +可以在 SELECT 语句中指定两个与查询缓存相关的选项 : + +- SQL_CACHE + + 如果查询结果是可缓存的,并且 query_cache_type 系统变量的值为 ON 或 DEMAND ,则缓存查询结果 。 + +- SQL_NO_CACHE + + 服务器不使用查询缓存。它既不检查查询缓存,也不检查结果是否已缓存,也不缓存查询结果。 + +例子: + +```SQL +SELECT SQL_CACHE id, name FROM customer; +SELECT SQL_NO_CACHE id, name FROM customer; +``` + +​ + +## 查询缓存失效的情况 + +1) SQL 语句不一致的情况, 要想命中查询缓存,查询的SQL语句必须一致。 + +```SQL +-- SQL1 : +select count(*) from tb_item; +-- SQL2 : +Select count(*) from tb_item; +``` + +2) 当查询语句中有一些不确定的时,则不会缓存。如 : now() , current_date() , curdate() , curtime() , rand() , uuid() , user() , database() 。 + +```SQL +select * from tb_item where updatetime < now() limit 1; +select user(); +select database(); +``` + +3) 不使用任何表查询语句。 + +```SQL +select 'A'; +``` + +4) 查询 mysql, information_schema或 performance_schema 系统数据库中的表时,不会走查询缓存。 + +```SQL +select * from information_schema.engines; +``` + +5) 在存储的函数,触发器或事件的主体内执行的查询。 + +6) 如果表更改,则使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除。这包括使用`MERGE`映射到已更改表的表的查询。一个表可以被许多类型的语句,如被改变 INSERT, UPDATE, DELETE, TRUNCATE TABLE, ALTER TABLE, DROP TABLE,或 DROP DATABASE 。 \ No newline at end of file diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/18.MySQL - \345\206\205\345\255\230\347\256\241\347\220\206.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/18.MySQL - \345\206\205\345\255\230\347\256\241\347\220\206.md" new file mode 100644 index 00000000..b0bb8553 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/18.MySQL - \345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -0,0 +1,89 @@ +--- +title: MySQL - 内存管理 +permalink: /mysql/memory-management/ +date: 2021-05-24 15:46:46 +--- + + + + + +- [Mysql内存管理及优化](#mysql%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E5%8F%8A%E4%BC%98%E5%8C%96) + - [内存优化原则](#%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96%E5%8E%9F%E5%88%99) + - [MyISAM 内存优化](#myisam-%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96) + - [InnoDB 内存优化](#innodb-%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96) + + + +# Mysql内存管理及优化 + +## 内存优化原则 + +1) 将尽量多的内存分配给MySQL做缓存,但要给操作系统和其他程序预留足够内存。 + +2) MyISAM 存储引擎的数据文件读取依赖于操作系统自身的IO缓存,因此,如果有MyISAM表,就要预留更多的内存给操作系统做IO缓存。 + +3) 排序区、连接区等缓存是分配给每个数据库会话(session)专用的,其默认值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发连接较高时会导致物理内存耗尽。 + + + +## MyISAM 内存优化 + +了解即可,我们平时用的都是 InnoDB + + + +myisam存储引擎使用 key_buffer 缓存索引块,加速myisam索引的读写速度。对于myisam表的数据块,mysql没有特别的缓存机制,完全依赖于操作系统的IO缓存。 + + + +- key_buffer_size + +key_buffer_size决定MyISAM索引块缓存区的大小,直接影响到MyISAM表的存取效率。可以在MySQL参数文件中设置key_buffer_size的值,对于一般MyISAM数据库,建议至少将1/4可用内存分配给key_buffer_size。 + +在 `/usr/my.cnf` 中做如下配置: + +```properties +key_buffer_size=512M +``` + +```sql +SHOW VARIABLES LIKE 'key_buffer_size' -- 查看大小 +``` + +- read_buffer_size + +如果需要经常顺序扫描myisam表,可以通过增大read_buffer_size的值来改善性能。但需要注意的是read_buffer_size是每个session独占的,如果默认值设置太大,就会造成内存浪费。 + + + +- read_rnd_buffer_size + +对于需要做排序的myisam表的查询,如带有order by子句的sql,适当增加 read_rnd_buffer_size 的值,可以改善此类的sql性能。但需要注意的是 read_rnd_buffer_size 是每个session独占的,如果默认值设置太大,就会造成内存浪费。 + + + +## InnoDB 内存优化 + +innodb用一块内存区做IO缓存池,该缓存池不仅用来缓存innodb的索引块,而且也用来缓存innodb的数据块。 + + + +- innodb_buffer_pool_size + +该变量决定了 innodb 存储引擎表数据和索引数据的最大缓存区大小。在保证操作系统及其他程序有足够内存可用的情况下,innodb_buffer_pool_size 的值越大,缓存命中率越高,访问InnoDB表需要的磁盘I/O 就越少,性能也就越高。 + +```properties +innodb_buffer_pool_size=512M +``` + + + +- innodb_log_buffer_size + +决定了 Innodb 重做日志缓存的大小,对于可能产生大量更新记录的大事务,增加innodb_log_buffer_size的大小,可以避免innodb在事务提交前就执行不必要的日志写入磁盘操作。 + +```properties +innodb_log_buffer_size=10M +``` + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/20.MySQL - \345\271\266\345\217\221\345\217\202\346\225\260.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/20.MySQL - \345\271\266\345\217\221\345\217\202\346\225\260.md" new file mode 100644 index 00000000..e3dcc16d --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/20.MySQL - \345\271\266\345\217\221\345\217\202\346\225\260.md" @@ -0,0 +1,58 @@ +--- +title: MySQL - 并发参数 +permalink: /mysql/concurrent-parameter/ +date: 2021-05-24 16:13:14 +--- + + + + + +- [max_connections](#max_connections) +- [back_log](#back_log) +- [table_open_cache](#table_open_cache) +- [thread_cache_size](#thread_cache_size) +- [innodb_lock_wait_timeout](#innodb_lock_wait_timeout) + + + + + +从实现上来说,MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在Mysql中,控制并发连接和线程的主要参数包括 max_connections、back_log、thread_cache_size、table_open_cahce。 + +## max_connections + +采用max_connections 控制允许连接到MySQL数据库的最大数量,默认值是 151。如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这是可以考虑增大max_connections 的值。 + +Mysql 最大可支持的连接数,取决于很多因素,包括给定操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度,期望的响应时间等。在Linux 平台下,性能好的服务器,支持 500-1000 个连接不是难事,需要根据服务器性能进行评估设定。 + + + +## back_log + +back_log 参数控制MySQL监听TCP端口时设置的积压请求栈大小。如果MySql的连接数达到 max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log,如果等待连接的数量超过back_log,将不被授予连接资源,将会报错。5.6.6 版本之前默认值为 50 , 之后的版本默认为 50 + (max_connections / 5), 但最大不超过900。 + +如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大 back_log 的值。 + + + +## table_open_cache + +该参数用来控制所有SQL语句执行线程可打开表缓存的数量, 而在执行SQL语句时,每一个SQL执行线程至少要打开 1 个表缓存。该参数的值应该根据设置的最大连接数 max_connections 以及每个连接执行关联查询中涉及的表的最大数量来设定 :max_connections x N ; + +```sql +SHOW VARIABLES LIKE 'table_open_cache' -- 查看大小 +``` + + + +## thread_cache_size + +为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,通过参数 thread_cache_size 可控制 MySQL 缓存客户服务线程的数量。 + + + +## innodb_lock_wait_timeout + +该参数是用来设置InnoDB 事务等待行锁的时间,默认值是50ms , 可以根据需要进行动态设置。对于需要快速反馈的业务系统来说,可以将行锁的等待时间调小,以避免事务长时间挂起; 对于后台运行的批量处理程序来说, 可以将行锁的等待时间调大, 以避免发生大的回滚操作。 + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/22.MySQL - \351\224\201\351\227\256\351\242\230.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/22.MySQL - \351\224\201\351\227\256\351\242\230.md" new file mode 100644 index 00000000..40ceb5be --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/22.MySQL - \351\224\201\351\227\256\351\242\230.md" @@ -0,0 +1,523 @@ +--- +title: MySQL - 锁问题 +permalink: /mysql/lock-question/ +date: 2021-05-24 16:15:57 +--- + + + + + +- [简介](#%E7%AE%80%E4%BB%8B) +- [MyISAM 表锁](#myisam-%E8%A1%A8%E9%94%81) + - [如何加表锁](#%E5%A6%82%E4%BD%95%E5%8A%A0%E8%A1%A8%E9%94%81) + - [读锁案例](#%E8%AF%BB%E9%94%81%E6%A1%88%E4%BE%8B) + - [写锁案例](#%E5%86%99%E9%94%81%E6%A1%88%E4%BE%8B) + - [结论](#%E7%BB%93%E8%AE%BA) + - [查看锁的争用情况](#%E6%9F%A5%E7%9C%8B%E9%94%81%E7%9A%84%E4%BA%89%E7%94%A8%E6%83%85%E5%86%B5) +- [InnoDB 行锁](#innodb-%E8%A1%8C%E9%94%81) + - [行锁介绍](#%E8%A1%8C%E9%94%81%E4%BB%8B%E7%BB%8D) + - [背景知识](#%E8%83%8C%E6%99%AF%E7%9F%A5%E8%AF%86) + - [InnoDB 的行锁模式](#innodb-%E7%9A%84%E8%A1%8C%E9%94%81%E6%A8%A1%E5%BC%8F) + - [行锁基本演示](#%E8%A1%8C%E9%94%81%E5%9F%BA%E6%9C%AC%E6%BC%94%E7%A4%BA) + - [无索引行锁升级为表锁](#%E6%97%A0%E7%B4%A2%E5%BC%95%E8%A1%8C%E9%94%81%E5%8D%87%E7%BA%A7%E4%B8%BA%E8%A1%A8%E9%94%81) + - [间隙锁危害](#%E9%97%B4%E9%9A%99%E9%94%81%E5%8D%B1%E5%AE%B3) + - [InnoDB 行锁争用情况](#innodb-%E8%A1%8C%E9%94%81%E4%BA%89%E7%94%A8%E6%83%85%E5%86%B5) + - [总结](#%E6%80%BB%E7%BB%93) + + + + + +## 简介 + +- **锁概述** + +锁是计算机协调多个进程或线程并发访问某一资源的机制(避免争抢)。 + +在数据库中,除传统的计算资源(如 CPU、RAM、I/O 等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。 + + + +- **锁分类:** + +从对数据操作的粒度分 : + +1) 表锁:操作时,会锁定整个表。 + +2) 行锁:操作时,会锁定当前操作行。 + +从对数据操作的类型分: + +1) 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。 + +2) 写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁。 + + + +- **Mysql 锁** + +相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。 + +下表中罗列出了各存储引擎对锁的支持情况: + +| 存储引擎 | 表级锁 | 行级锁 | 页面锁 | +| -------- | ------ | ------ | ------ | +| MyISAM | 支持 | 不支持 | 不支持 | +| InnoDB | 支持 | 支持 | 不支持 | +| MEMORY | 支持 | 不支持 | 不支持 | +| BDB | 支持 | 不支持 | 支持 | + +MySQL这3种锁的特性可大致归纳如下 : + +| 锁类型 | 特点 | +| ------ | ------------------------------------------------------------ | +| 表级锁 | 偏向MyISAM 存储引擎,开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 | +| 行级锁 | 偏向InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 | +| 页面锁 | 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 | + +从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!仅从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理(OLTP)系统。 + + + + +## MyISAM 表锁 + +MyISAM 存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型。 + + + +### 如何加表锁 + +MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。 + +```SQL +lock table table_name read; --加读锁 + +lock table table_name writ; --加写锁 +``` + + + +### 读锁案例 + +准备环境 + +```SQL +create database demo_03 default charset=utf8mb4; + +use demo_03; + +CREATE TABLE `tb_book` ( + `id` INT(11) auto_increment, + `name` VARCHAR(50) DEFAULT NULL, + `publish_time` DATE DEFAULT NULL, + `status` CHAR(1) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=myisam DEFAULT CHARSET=utf8 ; + +INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'java编程思想','2088-08-01','1'); +INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'solr编程思想','2088-08-08','0'); + + + +CREATE TABLE `tb_user` ( + `id` INT(11) auto_increment, + `name` VARCHAR(50) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=myisam DEFAULT CHARSET=utf8 ; + +INSERT INTO tb_user (id, name) VALUES(NULL,'令狐冲'); +INSERT INTO tb_user (id, name) VALUES(NULL,'田伯光'); + +``` + + + +客户端 一 : + +1)加 tb_book 表的读锁 + +```sql +lock table tb_book read; +``` + + + +2) 执行查询操作 + +```sql +select * from tb_book; +``` + +![1553906896564](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553906896564.png) + +可以正常执行 , 查询出数据。 + +客户端 二 : + +3) 执行查询操作 + +```sql +select * from tb_book; +``` + +![1553907044500](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553907044500.png) + + + +客户端 一 : + +4)查询未锁定的表 + +```sql +select name from tb_seller; +``` + +![1553908913515](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553908913515.png) + +客户端 二 : + +5)查询未锁定的表 + +```sql +select name from tb_seller; +``` + +![1553908973840](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553908973840.png) + +可以正常查询出未锁定的表; + + + +客户端 一 : + +6) 执行插入操作 + +```sql +insert into tb_book values(null,'Mysql高级','2088-01-01','1'); +``` + +![1553907198462](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553907198462.png) + +执行插入, 直接报错 , 由于当前tb_book 获得的是 读锁, 不能执行更新操作。 + + + +客户端 二 : + +7) 执行插入操作 + +```sql +insert into tb_book values(null,'Mysql高级','2088-01-01','1'); +``` + +![1553907403957](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553907403957.png) + + + +当在客户端一中释放锁指令 unlock tables 后 , 客户端二中的 inesrt 语句 , 立即执行 ; + +### 写锁案例 + +客户端 一 : + +1)给 tb_book 表的写锁 + +```sql +lock table tb_book write ; +``` + +2)执行查询操作 + +```sql +select * from tb_book ; +``` + +![1553907849829](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553907849829.png) + +查询操作执行成功; + +3)执行更新操作 + +```sql +update tb_book set name = 'java编程思想(第二版)' where id = 1; +``` + +![1553907875221](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553907875221.png) + +更新操作执行成功 ; + + + +客户端 二 : + +4)执行查询操作 + +``` +select * from tb_book ; +``` + +![1553908019755](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553908019755.png) + + + +当在客户端一中释放锁指令 unlock tables 后 , 客户端二中的 select 语句 , 立即执行 ; + +![1553908131373](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553908131373.png) + + + +### 结论 + +锁模式的相互兼容性如表中所示: + +![1553905621992](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553905621992.png) + +由上表可见: + +​ 1) 对MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求; + +​ 2) 对MyISAM 表的写操作,会都阻塞其他用户对同一表的读和写操作; + +​ 简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁,则既会阻塞读,又会阻塞写。 + + + +此外,MyISAM 的读写锁调度是写优先,这也是MyISAM不适合做写为主的表的存储引擎的原因。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。 + +### 查看锁的争用情况 + +``` sql +show open tables; +``` + +![1556443073322](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556443073322.png) + +In_user : 表当前被查询使用的次数。如果该数为零,则表是打开的,但是当前没有被使用。 + +Name_locked:表名称是否被锁定。名称锁定用于取消表或对表进行重命名等操作。 + + + +```sql +show status like 'Table_locks%'; +``` + +![1556443170082](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556443170082.png) + +Table_locks_immediate : 指的是能够立即获得表级锁的次数,每立即获取锁,值加1。 + +Table_locks_waited : 指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加1,此值高说明存在着较为严重的表级锁争用情况。 + +## InnoDB 行锁 + +### 行锁介绍 + +行锁特点 :偏向InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定力度最小,发生锁冲突的概率最低,并发度也最高。 + +InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁。 + + + +### 背景知识 + +**事务及其ACID属性** + +事务是由一组SQL语句组成的逻辑处理单元。 + +事务具有以下4个特性,简称为事务ACID属性。 + +| ACID属性 | 含义 | +| -------------------- | ------------------------------------------------------------ | +| 原子性(Atomicity) | 事务是一个原子操作单元,其对数据的修改,要么全部成功,要么全部失败。 | +| 一致性(Consistent) | 在事务开始和完成时,数据都必须保持一致状态。 | +| 隔离性(Isolation) | 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的 “独立” 环境下运行。 | +| 持久性(Durable) | 事务完成之后,对于数据的修改是永久的。 | + + + +**并发事务处理带来的问题** + +| 问题 | 含义 | +| ---------------------------------- | ------------------------------------------------------------ | +| 丢失更新(Lost Update) | 当两个或多个事务选择同一行,最初的事务修改的值,会被后面的事务修改的值覆盖。 | +| 脏读(Dirty Reads) | 当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。 | +| 不可重复读(Non-Repeatable Reads) | 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现和以前读出的数据不一致。 | +| 幻读(Phantom Reads) | 一个事务按照相同的查询条件重新读取以前查询过的数据,却发现其他事务插入了满足其查询条件的新数据。 | + + + +**事务隔离级别** + +为了解决上述提到的事务并发问题,数据库提供一定的事务隔离机制来解决这个问题。数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使用事务在一定程度上“串行化” 进行,这显然与“并发” 是矛盾的。 + +数据库的隔离级别有4个,由低到高依次为 Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决 脏写、脏读、不可重复读、幻读 这几类问题。 + +| 隔离级别 | 丢失更新 | 脏读 | 不可重复读 | 幻读 | +| ----------------------- | -------- | ---- | ---------- | ---- | +| Read uncommitted | × | √ | √ | √ | +| Read committed | × | × | √ | √ | +| Repeatable read(默认) | × | × | × | √ | +| Serializable | × | × | × | × | + +备注 : √ 代表可能出现 , × 代表不会出现 。 + +Mysql 的数据库的默认隔离级别为 Repeatable read , 查看方式: + +```sql +show variables like 'tx_isolation'; +``` + +![1554331600009](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554331600009.png) + + + +### InnoDB 的行锁模式 + +InnoDB 实现了以下两种类型的行锁。 + +- 共享锁(S):又称为读锁,简称 `S` 锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。 +- 排他锁(X):又称为写锁,简称 `X` 锁,排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。 + +对于 UPDATE、DELETE、INSERT 语句,InnoDB 会自动给涉及数据集加 排他锁(X) + +对于普通SELECT语句,InnoDB不会加任何锁; + + + +可以通过以下语句显示给记录集加共享锁或排他锁 。 + +```sql +-- 共享锁(S) +SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE +-- 排他锁(X) +SELECT * FROM table_name WHERE ... FOR UPDATE +``` + +### 行锁基本演示 + +准备 sql + +```sql +create table test_innodb_lock( + id int(11), + name varchar(16), + sex varchar(1) +)engine = innodb default charset=utf8; + +insert into test_innodb_lock values(1,'100','1'); +insert into test_innodb_lock values(3,'3','1'); +insert into test_innodb_lock values(4,'400','0'); +insert into test_innodb_lock values(5,'500','1'); +insert into test_innodb_lock values(6,'600','0'); +insert into test_innodb_lock values(7,'700','0'); +insert into test_innodb_lock values(8,'800','1'); +insert into test_innodb_lock values(9,'900','1'); +insert into test_innodb_lock values(1,'200','0'); + +create index idx_test_innodb_lock_id on test_innodb_lock(id); +create index idx_test_innodb_lock_name on test_innodb_lock(name); +``` + +开启两个会话 + +| Session-1 | Session-2 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| ![1554354615030](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554354615030.png)关闭自动提交功能 | ![1554354601867](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554354601867.png)关闭自动提交功能 | +| ![1554354713628](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554354713628.png)可以正常的查询出全部的数据 | ![1554354717336](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554354717336.png)可以正常的查询出全部的数据 | +| ![1554354830589](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554354830589.png)查询id 为3的数据 ; | ![1554354832708](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554354832708.png)获取id为3的数据 ; | +| ![1554382789984](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554382789984.png)更新id为3的数据,但是不提交; | ![1554382905352](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554382905352.png) 更新id为3 的数据, 出于等待状态 | +| ![1554382977653](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554382977653.png)通过commit, 提交事务 | ![1554383044542](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554383044542.png) 解除阻塞,更新正常进行 | +| 以上, 操作的都是同一行的数据,接下来,演示不同行的数据 : | | +| ![1554385220580](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554385220580.png) 更新id为3数据,正常的获取到行锁 ,执行更新 | ![1554385236768](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554385236768.png) 由于与 Session-1 操作不是同一行,获取当前行锁,执行更新; | + +### 无索引行锁升级为表锁 + +如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟表锁一样。 + +```sql + --查看当前表的索引 : + show index from test_innodb_lock ; +``` + +![1554385956215](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554385956215.png) + +| Session-1 | Session-2 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| ![1554386287454](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554386287454.png)关闭事务的自动提交 | ![1554386312524](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554386312524.png)关闭事务的自动提交 | +| ![1554386654793](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554386654793-1621851044615.png)执行更新语句 | ![1554386685610](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554386685610.png)执行更新语句, 但处于阻塞状态 | +| ![1554386721653](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554386721653.png)提交事务 | ![1554386750004](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554386750004.png)解除阻塞,执行更新成功 | +| | ![1554386804807](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554386804807.png)执行提交操作 | + +由于执行更新时,name字段本来为varchar类型, 我们是作为数字类型使用,存在类型转换,索引失效,最终行锁变为表锁 ; + + + +### 间隙锁危害 + +当我们用范围条件,而不是使用相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据进行加锁; 对于键值在条件范围内但并不存在的记录,叫做 "间隙(GAP)" , InnoDB也会对这个 "间隙" 加锁,这种锁机制就是所谓的 间隙锁(Next-Key锁) 。 + +示例 : + +| Session-1 | Session-2 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| ![1554387987130](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554387987130.png)关闭事务自动提交 ! | ![](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210524221100.png)关闭事务自动提交 | +| ![1554388492478](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554388492478.png)根据id范围更新数据 | | +| | ![1554388515936](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554388515936.png)插入id为2的记录, 出于阻塞状态! | +| ![1554388149305](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554388149305.png)提交事务 | | +| | ![1554388548562](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554388548562.png)解除阻塞 , 执行插入操作 | +| | 提交事务 | + + + +### InnoDB 行锁争用情况 + +```sql +show status like 'innodb_row_lock%'; +``` + +![1556455943670](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1556455943670.png) + +- Innodb_row_lock_current_waits + + 当前正在等待锁定的数量 + +- Innodb_row_lock_time + + 从系统启动到现在锁定总时间长度 + +- Innodb_row_lock_time_avg + + 每次等待所花平均时长 + +- Innodb_row_lock_time_max + + 从系统启动到现在等待最长的一次所花的时间 + +- Innodb_row_lock_waits + + 系统启动后到现在总共等待的次数 + +当等待的次数很高,而且每次等待的时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。 + + + + + +### 总结 + +InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高一些,但是在整体并发处理能力方面要远远由于MyISAM的表锁的。当系统并发量较高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势。 + +但是,InnoDB的行级锁同样也有其脆弱的一面,当我们使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。 + + + +优化建议: + +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁。 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件,及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(但是需要业务层面满足需求) + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/24.MySQL - \345\270\270\347\224\250sql\346\212\200\345\267\247.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/24.MySQL - \345\270\270\347\224\250sql\346\212\200\345\267\247.md" new file mode 100644 index 00000000..f04c817c --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/24.MySQL - \345\270\270\347\224\250sql\346\212\200\345\267\247.md" @@ -0,0 +1,176 @@ +--- +title: MySQL - 常用sql技巧 +permalink: /mysql/common-use-sql-skill/ +date: 2021-05-24 19:06:10 +--- + + + + + +- [SQL执行顺序](#sql%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F) +- [正则表达式使用](#%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E4%BD%BF%E7%94%A8) +- [MySQL 常用函数](#mysql-%E5%B8%B8%E7%94%A8%E5%87%BD%E6%95%B0) + + + + + +## SQL执行顺序 + +编写顺序 + +```SQL +SELECT DISTINCT + + +ORDER BY + +LIMIT +``` + + + +## 正则表达式使用 + +正则表达式(Regular Expression)是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。 + +| 符号 | 含义 | +| ------ | ----------------------------- | +| ^ | 在字符串开始处进行匹配 | +| $ | 在字符串末尾处进行匹配 | +| . | 匹配任意单个字符, 包括换行符 | +| [...] | 匹配出括号内的任意字符 | +| [^...] | 匹配不出括号内的任意字符 | +| a* | 匹配零个或者多个a(包括空串) | +| a+ | 匹配一个或者多个a(不包括空串) | +| a? | 匹配零个或者一个a | +| a1\|a2 | 匹配a1或a2 | +| a(m) | 匹配m个a | +| a(m,) | 至少匹配m个a | +| a(m,n) | 匹配m个a 到 n个a | +| a(,n) | 匹配0到n个a | +| (...) | 将模式元素组成单一元素 | + +``` +select * from emp where name regexp '^T'; + +select * from emp where name regexp '2$'; + +select * from emp where name regexp '[uvw]'; +``` + + + +## MySQL 常用函数 + +数字函数 + +| 函数名称 | 作 用 | +| --------------- | ---------------------------------------------------------- | +| ABS | 求绝对值 | +| SQRT | 求二次方根 | +| MOD | 求余数 | +| CEIL 和 CEILING | 两个函数功能相同,都是返回不小于参数的最小整数,即向上取整 | +| FLOOR | 向下取整,返回值转化为一个BIGINT | +| RAND | 生成一个0~1之间的随机数,传入整数参数是,用来产生重复序列 | +| ROUND | 对所传参数进行四舍五入 | +| SIGN | 返回参数的符号 | +| POW 和 POWER | 两个函数的功能相同,都是所传参数的次方的结果值 | +| SIN | 求正弦值 | +| ASIN | 求反正弦值,与函数 SIN 互为反函数 | +| COS | 求余弦值 | +| ACOS | 求反余弦值,与函数 COS 互为反函数 | +| TAN | 求正切值 | +| ATAN | 求反正切值,与函数 TAN 互为反函数 | +| COT | 求余切值 | + +字符串函数 + +| 函数名称 | 作 用 | +| --------- | ------------------------------------------------------------ | +| LENGTH | 计算字符串长度函数,返回字符串的字节长度 | +| CONCAT | 合并字符串函数,返回结果为连接参数产生的字符串,参数可以使一个或多个 | +| INSERT | 替换字符串函数 | +| LOWER | 将字符串中的字母转换为小写 | +| UPPER | 将字符串中的字母转换为大写 | +| LEFT | 从左侧字截取符串,返回字符串左边的若干个字符 | +| RIGHT | 从右侧字截取符串,返回字符串右边的若干个字符 | +| TRIM | 删除字符串左右两侧的空格 | +| REPLACE | 字符串替换函数,返回替换后的新字符串 | +| SUBSTRING | 截取字符串,返回从指定位置开始的指定长度的字符换 | +| REVERSE | 字符串反转(逆序)函数,返回与原始字符串顺序相反的字符串 | + +日期函数 + +| 函数名称 | 作 用 | +| ----------------------- | ------------------------------------------------------------ | +| CURDATE 和 CURRENT_DATE | 两个函数作用相同,返回当前系统的日期值 | +| CURTIME 和 CURRENT_TIME | 两个函数作用相同,返回当前系统的时间值 | +| NOW 和 SYSDATE | 两个函数作用相同,返回当前系统的日期和时间值 | +| MONTH | 获取指定日期中的月份 | +| MONTHNAME | 获取指定日期中的月份英文名称 | +| DAYNAME | 获取指定曰期对应的星期几的英文名称 | +| DAYOFWEEK | 获取指定日期对应的一周的索引位置值 | +| WEEK | 获取指定日期是一年中的第几周,返回值的范围是否为 0〜52 或 1〜53 | +| DAYOFYEAR | 获取指定曰期是一年中的第几天,返回值范围是1~366 | +| DAYOFMONTH | 获取指定日期是一个月中是第几天,返回值范围是1~31 | +| YEAR | 获取年份,返回值范围是 1970〜2069 | +| TIME_TO_SEC | 将时间参数转换为秒数 | +| SEC_TO_TIME | 将秒数转换为时间,与TIME_TO_SEC 互为反函数 | +| DATE_ADD 和 ADDDATE | 两个函数功能相同,都是向日期添加指定的时间间隔 | +| DATE_SUB 和 SUBDATE | 两个函数功能相同,都是向日期减去指定的时间间隔 | +| ADDTIME | 时间加法运算,在原始时间上添加指定的时间 | +| SUBTIME | 时间减法运算,在原始时间上减去指定的时间 | +| DATEDIFF | 获取两个日期之间间隔,返回参数 1 减去参数 2 的值 | +| DATE_FORMAT | 格式化指定的日期,根据参数返回指定格式的值 | +| WEEKDAY | 获取指定日期在一周内的对应的工作日索引 | + +聚合函数 + +| 函数名称 | 作用 | +| -------- | -------------------------------- | +| MAX | 查询指定列的最大值 | +| MIN | 查询指定列的最小值 | +| COUNT | 统计查询结果的行数 | +| SUM | 求和,返回指定列的总和 | +| AVG | 求平均值,返回指定列数据的平均值 | + + + + + + + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/26.MySQL - \345\270\270\347\224\250\345\267\245\345\205\267.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/26.MySQL - \345\270\270\347\224\250\345\267\245\345\205\267.md" new file mode 100644 index 00000000..390d720f --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/26.MySQL - \345\270\270\347\224\250\345\267\245\345\205\267.md" @@ -0,0 +1,211 @@ +--- +title: MySQL - 常用工具 +permalink: /mysql/common-tools/ +date: 2021-05-25 16:23:45 +--- + + + + + + + +- [mysql](#mysql) +- [mysqladmin](#mysqladmin) +- [mysqlbinlog](#mysqlbinlog) +- [mysqldump](#mysqldump) +- [mysqlimport和source](#mysqlimport%E5%92%8Csource) +- [mysqlshow](#mysqlshow) + + + +## mysql + +该mysql不是指mysql服务,而是指mysql的客户端工具。 + +**语法 :** + +```sh +mysql [options] [database] +``` + +**连接选项:** + +| 参数 | 说明 | +| ------------------ | ------------------ | +| -u, --user=txt | 指定用户名 | +| -p, --password=txt | 指定密码 | +| -h, --host=txt | 指定服务器IP或域名 | +| -p, --port=# | 指定连接端口 | + + +示例 : + +```sql +mysql -h 127.0.0.1 -P 3306 -u root -p + +mysql -h127.0.0.1 -P3306 -uroot -p2143 -- 可不加空格 +``` + +**执行选项** + +| 参数 | 说明 | +| ------------------ | ----------------- | +| -e, --execute=name | 执行SQL语句并退出 | + +此选项可以在Mysql客户端执行SQL语句,而不用连接到MySQL数据库再执行,对于一些批处理脚本,这种方式尤其方便。 + +示例: + +```sh +mysql -uroot -p2143 db01 -e "select * from tb_book"; +``` + +![1555325632715](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555325632715.png) + +## mysqladmin + +mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等。 + +查看帮助文档指令 + +```sh +mysqladmin --help +``` + + + +![1555326108697](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1555326108697.png) + +示例 : + +```sh +mysqladmin -uroot -p2143 create 'test01'; +mysqladmin -uroot -p2143 drop 'test01'; +mysqladmin -uroot -p2143 version; +``` + +## mysqlbinlog + +由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文本的文本格式,就会使用到mysqlbinlog 日志管理工具。 + +语法 : + +```sh +mysqlbinlog [options] log-files1 log-files2 ... +``` + +| 参数选项 | 说明 | +| --------------------------------------------- | -------------------------------------------- | +| -d, --database=name | 指定数据库名称,只列出指定的数据库相关操作。 | +| -o, --offset=# | 忽略掉日志中的前n行命令。 | +| -r,--result-file=name | 将输出的文本格式日志输出到指定文件。 | +| -s, --short-form | 显示简单格式, 省略掉一些信息。 | +| --start-datatime=date1 --stop-datetime=date2 | 指定日期间隔内的所有日志。 | +| --start-position=pos1 --stop-position=pos2 | 指定位置间隔内的所有日志。 | + + + +## mysqldump + +mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插入表的SQL语句。 + +语法 : + +```sh +mysqldump [options] db_name [tables] + +mysqldump [options] --database/-B db1 [db2 db3...] + +mysqldump [options] --all-databases/-A +``` + +**连接选项** + +| 参数 | 说明 | +| --------------------- | ------------------ | +| -u, --user=name | 指定用户名 | +| -p, --password[=name] | 指定密码 | +| -h, --host=name | 指定服务器IP或域名 | +| -P, --port=# | 指定连接端口 | + +​ + +**输出内容选项** + + + +| 参数 | 说明 | +| -------------------- | ------------------------------------------------------------ | +| --add-drop-database | 在每个数据库创建语句前加上 Drop database 语句 | +| --add-drop-table | 在每个表创建语句前加上 Drop table 语句 , 默认开启 ; 不开启 (--skip-add-drop-table) | +| -n, --no-create-db | 不包含数据库的创建语句 | +| -t, --no-create-info | 不包含数据表的创建语句 | +| -d --no-data | 不包含数据 | +| -T, --tab=name | 自动生成两个文件:
一个.sql文件,创建表结构的语句;
一个.txt文件,数据文件,相当于 select into outfile ; | + +示例 : +```sh +mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table > a + +mysqldump -uroot -p2143 -T /tmp test city +``` + +## mysqlimport和source + +mysqlimport 是客户端数据导入工具,用来导入 mysqldump 加 -T 参数后导出的文本文件。 + +也就是表的数据内容(txt内容) + +语法: + +```sh +mysqlimport [options] db_name textfile1 [textfile2...] +``` + +示例: + +```sh +mysqlimport -uroot -p2143 test /tmp/city.txt +``` + + + +如果需要导入sql文件,可以使用mysql中的source 指令 : + +```sh +source /root/tb_book.sql +``` + +## mysqlshow + +mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。 + +语法: + +```sh +mysqlshow [options] [db_name [table_name [col_name]]] +``` + +参数: + +| 参数 | 说明 | +| ------- | --------------------------------------------------- | +| --count | 显示数据库及表的统计信息(数据库,表 均可以不指定) | +| -i | 显示指定数据库或者指定表的状态信息 | + + + +示例: + +```sh +#查询每个数据库的表的数量及表中记录的数量 +mysqlshow -uroot -p2143 --count + +#查询test库中每个表中的字段书,及行数 +mysqlshow -uroot -p2143 test --count + +#查询test库中book表的详细情况 +mysqlshow -uroot -p2143 test book --count +``` + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/28.MySQL - \346\227\245\345\277\227.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/28.MySQL - \346\227\245\345\277\227.md" new file mode 100644 index 00000000..33bbf435 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/28.MySQL - \346\227\245\345\277\227.md" @@ -0,0 +1,334 @@ +--- +title: MySQL - 日志 +permalink: /mysql/log/ +date: 2021-05-25 16:50:59 +--- + + + + + + + +- [错误日志](#%E9%94%99%E8%AF%AF%E6%97%A5%E5%BF%97) +- [二进制日志](#%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%97%A5%E5%BF%97) + - [概述](#%E6%A6%82%E8%BF%B0) + - [日志格式](#%E6%97%A5%E5%BF%97%E6%A0%BC%E5%BC%8F) + - [日志读取](#%E6%97%A5%E5%BF%97%E8%AF%BB%E5%8F%96) + - [日志删除](#%E6%97%A5%E5%BF%97%E5%88%A0%E9%99%A4) +- [查询日志](#%E6%9F%A5%E8%AF%A2%E6%97%A5%E5%BF%97) +- [慢查询日志](#%E6%85%A2%E6%9F%A5%E8%AF%A2%E6%97%A5%E5%BF%97) + - [文件位置和格式](#%E6%96%87%E4%BB%B6%E4%BD%8D%E7%BD%AE%E5%92%8C%E6%A0%BC%E5%BC%8F) + - [日志的读取](#%E6%97%A5%E5%BF%97%E7%9A%84%E8%AF%BB%E5%8F%96) + + + +## 错误日志 + +错误日志是 MySQL 中最重要的日志之一,它记录了当 `mysqld` 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志。 + +该日志是默认开启的 , 默认存放目录为 mysql 的数据目录:`var/lib/mysql ` + +默认的日志文件名为 `hostname.err`(hostname是主机名)。 + + + +查看日志位置指令 : + +```sh +show variables like 'log_error%'; +``` + +![1553993244446](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553993244446.png) + + + +查看日志内容 : + +```sh +tail -f /var/lib/mysql/xaxh-server.err +``` + +![1553993537874](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1553993537874.png) + +## 二进制日志 + +### 概述 + +二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是**不包括数据查询语句**。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制, 就是通过该 binlog 实现的。 + +二进制日志,默认情况下是没有开启的,需要到MySQL的配置文件中开启,并配置MySQL日志的格式。 + +配置文件位置 : `/usr/my.cnf` + +日志存放位置 : 配置时,给定了文件名但是没有指定路径,日志默认写入Mysql的数据目录。 + +```properties +#配置开启 binlog 日志, +#日志的文件前缀为 mysqlbin +#生成的文件名如 : mysqlbin.000001,mysqlbin.000002 +log_bin=mysqlbin + +#配置二进制日志的格式 +binlog_format=STATEMENT +``` + + + +### 日志格式 + +**STATEMENT** + +该日志格式在日志文件中记录的都是SQL语句(statement),每一条对数据进行修改的 SQL 都会记录在日志文件中,通过 Mysql 提供的 mysqlbinlog 工具,可以清晰的查看到每条语句的文本。 + +主从复制的时候,从库(slave)会将日志解析为原文本,并在从库重新执行一次。 + + + +**ROW** + +该日志格式在日志文件中记录的是每一行的数据变更,而不是记录SQL语句。 + +比如,执行SQL语句 : `update tb_book set status='1' ` + +如果是 STATEMENT 日志格式,在日志中会记录一行SQL文件; + +如果是 ROW,由于是对全表进行更新,也就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更。 + + + +**MIXED** + +这是目前MySQL默认的日志格式,即混合了 STATEMENT 和 ROW 两种格式。 + +默认情况下采用STATEMENT,但是在一些特殊情况下采用ROW来进行记录。 + +MIXED 格式能尽量利用两种模式的优点,而避开他们的缺点。 + + + +### 日志读取 + +由于日志以二进制方式存储,不能直接读取,需要用 mysqlbinlog 工具来查看,语法如下 : + +```sh +mysqlbinlog log-file; +``` + + + +**查看STATEMENT格式日志** + +执行插入语句 : + +```SQL +insert into tb_book values(null,'Lucene','2088-05-01','0'); +``` + + 查看日志文件 : + +![1554079717375](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554079717375.png) + +`mysqlbin.index` : 该文件是日志索引文件 , 记录日志的文件名; + +`mysqlbing.000001` :日志文件 + +查看日志内容 : + +```sh +mysqlbinlog mysqlbing.000001; +``` + +![1554080016778](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554080016778.png) + + + +**查看ROW格式日志** + +配置 : + +```properties +#配置开启 binlog 日志, +#日志的文件前缀为 mysqlbin +#生成的文件名如 : mysqlbin.000001,mysqlbin.000002 +log_bin=mysqlbin + +#配置二进制日志的格式 +binlog_format=ROW + +``` + +插入数据 : + +```sql +insert into tb_book values(null,'SpringCloud实战','2088-05-05','0'); +``` + +如果日志格式是 ROW , 直接查看数据 , 是查看不懂的 ; 可以在 mysqlbinlog 后面加上参数 -vv + +```sh +mysqlbinlog -vv mysqlbin.000002 +``` + +![1554095452022](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554095452022.png) + + + +### 日志删除 + +对于比较繁忙的系统,由于每天生成日志量大 ,这些日志如果长时间不清楚,将会占用大量的磁盘空间。下面我们将会讲解几种删除日志的常见方法 : + +- **方式一** + +通过 `Reset Master` 指令删除全部 binlog 日志,删除之后,日志编号,将从 xxxx.000001重新开始 。 + +查询之前 ,先查询下日志文件 : + +![1554118609489](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554118609489.png) + +执行删除日志指令: + +```sh +Reset Master +``` + +执行之后, 查看日志文件 : + +![1554118675264](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554118675264.png) + + + +- **方式二** + +执行指令 ``` purge master logs to 'mysqlbin.******'``` ,该命令将删除 ``` ******``` 编号之前的所有日志。 + + + +- **方式三** + +执行指令: ``` purge master logs before 'yyyy-mm-dd hh24:mi:ss'``` + +该命令将删除日志为 "yyyy-mm-dd hh24:mi:ss" 之前产生的所有日志 。 + + + +- **方式四** + +设置参数 `--expire_logs_days=#` ,此参数的含义是设置日志的过期天数, 过了指定的天数后日志将会被自动删除,这样将有利于减少DBA 管理日志的工作量。 + +配置如下 : + +![1554125506938](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554125506938.png) + + + +## 查询日志 + +查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的SQL语句。 + +默认情况下, 查询日志是未开启的。如果需要开启查询日志,可以设置以下配置 : + +```properties +#该选项用来开启查询日志 +#可选值:0 或者 1 ;0 代表关闭, 1 代表开启 +general_log=1 + +#设置日志的文件名,如果没有指定,默认的文件名为 host_name.log +general_log_file=file_name +``` + +在 mysql 的配置文件 `/usr/my.cnf` 中配置如下内容 : + +![1554128184632](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554128184632.png) + + + +配置完毕之后,在数据库执行以下操作 : + +```sql +select * from tb_book; +select * from tb_book where id = 1; +update tb_book set name = 'lucene入门指南' where id = 5; +select * from tb_book where id < 8; +``` + + + +执行完毕之后, 再次来查询日志文件 : + +![1554128089851](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554128089851.png) + +## 慢查询日志 + +慢查询日志记录了所有执行时间超过参数 long_query_time 设置值并且扫描记录数不小于 min_examined_row_limit 的所有的SQL语句的日志。 + +long_query_time 默认为 10 秒,最小为 0, 精度可以到微秒。 + + + +### 文件位置和格式 + +慢查询日志默认是关闭的 。可以通过两个参数来控制慢查询日志 : + +```properties +# 该参数用来控制慢查询日志是否开启 +#可取值: 1 和 0 , 1 代表开启, 0 代表关闭 +slow_query_log=1 + +# 该参数用来指定慢查询日志的文件名 +slow_query_log_file=slow_query.log + +# 该选项用来配置查询的时间限制 +#超过这个时间将认为值慢查询,将需要进行日志记录 +#默认10s +long_query_time=10 +``` + + + +### 日志的读取 + +和错误日志、查询日志一样,慢查询日志记录的格式也是纯文本,可以被直接读取。 + +1) 查询 long_query_time 的值。 + +![1554130333472](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554130333472.png) + + + +2) 执行查询操作 + +```sql +select id, title,price,num ,status from tb_item where id = 1; +``` + +![1554130448709](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554130448709.png) + +由于该语句执行时间很短,为0s , 所以不会记录在慢查询日志中。 + + + +```sql +select * from tb_item where title like '%阿尔卡特 (OT-927) 炭黑 联通3G手机 双卡双待165454%' ; +``` + +![1554130532577](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554130532577.png) + +该SQL语句 , 执行时长为 26.77s ,超过10s , 所以会记录在慢查询日志文件中。 + + + +3) 查看慢查询日志文件 + +直接通过cat 指令查询该日志文件 : + +![1554130669360](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554130669360.png) + + + +如果慢查询日志内容很多, 直接查看文件,比较麻烦 + +这个时候可以借助于 mysql 自带的 mysqldumpslow 工具, 来对慢查询日志进行分类汇总。 + +![1554130856485](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554130856485.png) + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/30.MySQL - \344\270\273\344\273\216\345\244\215\345\210\266.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/30.MySQL - \344\270\273\344\273\216\345\244\215\345\210\266.md" new file mode 100644 index 00000000..0347f895 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/05.MySQL/30.MySQL - \344\270\273\344\273\216\345\244\215\345\210\266.md" @@ -0,0 +1,202 @@ +--- +title: MySQL - 主从复制 +permalink: /mysql/copy/ +date: 2021-05-25 19:01:15 +--- + + + + + + + +- [复制概述](#%E5%A4%8D%E5%88%B6%E6%A6%82%E8%BF%B0) +- [复制原理](#%E5%A4%8D%E5%88%B6%E5%8E%9F%E7%90%86) +- [复制优势](#%E5%A4%8D%E5%88%B6%E4%BC%98%E5%8A%BF) +- [搭建步骤](#%E6%90%AD%E5%BB%BA%E6%AD%A5%E9%AA%A4) + - [master](#master) + - [slave](#slave) + - [验证同步操作](#%E9%AA%8C%E8%AF%81%E5%90%8C%E6%AD%A5%E6%93%8D%E4%BD%9C) + + + +## 复制概述 + +复制是指将主数据库的DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步。 + +MySQL支持一台主库同时向多台从库进行复制, 从库同时也可以作为其他从服务器的主库,实现链状复制。 + + + +## 复制原理 + +MySQL 的主从复制原理如下。 + +![1554423698190](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1.jpg) + +从上层来看,复制分成三步: + +- Master 主库在事务提交时,会把数据变更作为时间 Events 记录在二进制日志文件 Binlog 中。 +- 主库推送二进制日志文件 Binlog 中的日志事件到从库的中继日志 Relay Log 。 + +- slave 重做中继日志中的事件,将改变反映它自己的数据。 + + + +## 复制优势 + +MySQL 复制的有点主要包含以下三个方面: + +- 主库出现问题,可以快速切换到从库提供服务。 + +- 可以在从库上执行查询操作,从主库中更新,实现读写分离,降低主库的访问压力。 + +- 可以在从库中执行备份,以避免备份期间影响主库的服务。 + + + +## 搭建步骤 + +### master + +1) 在master 的配置文件(/usr/my.cnf)中,配置如下内容: + +```properties +#mysql 服务ID,保证整个集群环境中唯一 +server-id=1 + +#mysql binlog 日志的存储路径和文件名 +log-bin=/var/lib/mysql/mysqlbin + +#错误日志,默认已经开启 +#log-err + +#mysql的安装目录 +#basedir + +#mysql的临时目录 +#tmpdir + +#mysql的数据存放目录 +#datadir + +#是否只读,1 代表只读, 0 代表读写 +read-only=0 + +#忽略的数据, 指不需要同步的数据库 +binlog-ignore-db=mysql + +#指定同步的数据库 +#binlog-do-db=db01 +``` + +2) 执行完毕之后,需要重启Mysql: + +```sh +service mysql restart +#有的是这个 +service mysqld restart +``` + +3) 创建同步数据的账户,并且进行授权操作: + +```sql +grant replication slave on *.* to 'itcast'@'192.168.192.131' identified by 'itcast'; + +flush privileges; +``` + +4) 查看master状态: + +```sql +show master status; +``` + +![1554477759735](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554477759735.png) + + + + +| 字段 | 含义 | +| ---------------- | ------------------------------ | +| File | 从哪个日志文件开始推送日志文件 | +| Position | 从哪个位置开始推送日志 | +| Binlog_Ignore_DB | 指定不需要同步的数据库 | + + + +### slave + +1) 在 slave 端配置文件中,配置如下内容: + +```properties +#mysql服务端ID,唯一 +server-id=2 + +#指定binlog日志 +log-bin=/var/lib/mysql/mysqlbin +``` + +2) 执行完毕之后,需要重启Mysql: + +```sh +service mysql restart; +``` + +3) 执行如下指令 : + +```sql +change master to master_host= '192.168.192.130', master_user='itcast', master_password='itcast', master_log_file='mysqlbin.000001', master_log_pos=413; +``` + +指定当前从库对应的主库的IP地址,用户名,密码,从哪个日志文件开始的那个位置开始同步推送日志。 + +4) 开启同步操作 + +```sql +start slave; + +show slave status; +``` + +![1554479387365](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554479387365.png) + +5) 停止同步操作 + +```sql +stop slave; +``` + + + +### 验证同步操作 + +1) 在主库中创建数据库,创建表,并插入数据 : + +```sql +create database db01; + +user db01; + +create table user( + id int(11) not null auto_increment, + name varchar(50) not null, + sex varchar(1), + primary key (id) +)engine=innodb default charset=utf8; + +insert into user(id,name,sex) values(null,'Tom','1'); +insert into user(id,name,sex) values(null,'Trigger','0'); +insert into user(id,name,sex) values(null,'Dawn','1'); +``` + +2) 在从库中查询数据,进行验证 : + +在从库中,可以查看到刚才创建的数据库: + +![1554544658640](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554544658640.png) + +在该数据库中,查询user表中的数据: + +![1554544679538](https://cdn.jsdelivr.net/gh/oddfar/static/img/MySQL高级.assets/1554544679538.png) + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/01.Redis\345\205\245\351\227\250.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/01.Redis\345\205\245\351\227\250.md" new file mode 100644 index 00000000..6b1db3e9 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/01.Redis\345\205\245\351\227\250.md" @@ -0,0 +1,257 @@ +--- +title: Redis入门 +date: 2021-05-17 16:10:19 +permalink: /redis/study-note/1/ +--- +

狂神redis视频教程视频教程:https://www.bilibili.com/video/BV1S54y1R7SB

+ + +## 概述 + +Redis:REmote DIctionary Server(远程字典服务器) + +是完全开源免费的,用C语言编写的,遵守BSD协议,是一个高性能的(Key/Value)分布式内存数据 库,基于内存运行,并支持持久化的NoSQL数据库,是当前最热门的NoSQL数据库之一,也被人们称为数据结构服务器 + +Redis与其他key-value缓存产品有以下三个特点 + +- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。 +- Redis不仅仅支持简单的 key-value 类型的数据,同时还提供list、set、zset、hash等数据结构的存储。 +- Redis支持数据的备份,即master-slave模式的数据备份。 + +## 常用网站 + +- 官网 + + https://redis.io/ + +- 中文网 + + http://www.redis.cn + + + +## 安装Redis + +由于企业里面做Redis开发,99%都是Linux版的运用和安装,几乎不会涉及到Windows版,所以这里就以linux版为主,可以自己去测试玩玩,Windows安装及使用教程:https://www.cnblogs.com/xing-nb/p/12146449.html + + + +linux直接去官网下载:https://redis.io/download + +安装步骤(基于当时最新版6.2.1): + +1. 下载压缩包,放置Linux的目录下 /opt + +2. 在/opt 目录下解压,命令 : `tar -zxvf redis-6.2.1.tar.gz` + +3. 解压完成后出现文件夹:redis-6.2.1 + +4. 进入目录: `cd redis-6.2.1` + +5. 在 redis-6.2.1 目录下执行 `make` 命令 + + 运行make命令时故意出现的错误解析: + + 1. 安装gcc (gcc是linux下的一个编译程序,是c程序的编译工具) + + 能上网: yum install gcc-c++ + + 版本测试: gcc-v + + 2. 二次make + + 3. Jemalloc/jemalloc.h:没有那个文件或目录 + + 运行 make distclean 之后再make + + 4. Redis Test(可以不用执行) + +6. 如果make完成后执行 `make install` + +7. 查看默认安装目录:`cd /usr/local/bin` + + /usr 这是一个非常重要的目录,类似于windows下的Program Files,存放用户的程序 + + ![image-20210406233231151](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210406233231151.png) + +8. redis默认不是后台启动,修改文件 + + 一般我们在 /usr/local/bin 目录下,创建myconfig目录,存放我们的配置文件 + + ```bash + cd /usr/local/bin + mkdir myconfig #创建目录 + + #拷贝配置文件 + cd /opt/redis-6.2.1 + cp redis.conf /usr/local/bin # 拷一个备份,养成良好的习惯,我们就修改这个文件 + # 修改配置保证可以后台应用 + vim redis.conf + /daemonize #查找 + :wq #保存 + ``` + + ![image-20210406234601005](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210406234601005.png) + + - A、redis.conf配置文件中daemonize守护线程,默认是NO。 + - B、daemonize是用来指定redis是否要用守护线程的方式启动。 + + **daemonize 设置yes或者no区别** + + - daemonize:yes + + redis采用的是单进程多线程的模式。当redis.conf中选项daemonize设置成yes时,代表开启守护进程模式。在该模式下,redis会在后台运行,并将进程pid号写入至redis.conf选项 pidfile设置的文件中,此时redis将一直运行,除非手动kill该进程。 + + - daemonize:no + + 当daemonize选项设置成no时,当前界面将进入redis的命令行界面,exit强制退出或者关闭连接工具(putty,xshell等)都会导致redis进程退出。 + +9. 启动测试一下! + + - 启动redis服务 + + ```bash + cd /usr/local/bin + redis-server myconfig/redis.conf + ``` + +- redis客户端连接 + + ```bash + redis-cli -p 6379 + ``` + + 观察地址的变化,如果连接成功,是直接连上的,redis默认端口号 6379 + + ![image](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210407105934960.png) + +- 执行ping、get和set操作、退出 + + ```bash + 127.0.0.1:6379> ping + PONG + 127.0.0.1:6379> get hello + (nil) + 127.0.0.1:6379> set hello zhiyuan + OK + 127.0.0.1:6379> get hello + "zhiyuan" + ``` + + ![image-20210407001739971](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210407001739971.png) + +- 关闭连接 + + ```bash + 127.0.0.1:6379> shutdown + not connected> exit + ``` + + +可以使用指令`ps -ef|grep redis `显示系统当前redis 进程信息,查看开启和关闭连接的变化 + +## 基础知识说明 + +### redis压力测试 + +Redis-benchmark是官方自带的Redis性能测试工具,可以有效的测试Redis服务的性能。 + +redis 性能测试工具可选参数如下所示: + +| 序号 | 选项 | 描述 | 默认值 | +| :--- | :-------- | :----------------------------------------- | :-------- | +| 1 | **-h** | 指定服务器主机名 | 127.0.0.1 | +| 2 | **-p** | 指定服务器端口 | 6379 | +| 3 | **-s** | 指定服务器 socket | | +| 4 | **-c** | 指定并发连接数 | 50 | +| 5 | **-n** | 指定请求数 | 10000 | +| 6 | **-d** | 以字节的形式指定 SET/GET 值的数据大小 | 2 | +| 7 | **-k** | 1=keep alive 0=reconnect | 1 | +| 8 | **-r** | SET/GET/INCR 使用随机 key, SADD 使用随机值 | | +| 9 | **-P** | 通过管道传输 numreq 请求 | 1 | +| 10 | **-q** | 强制退出 redis。仅显示 query/sec 值 | | +| 11 | **--csv** | 以 CSV 格式输出 | | +| 12 | **-l** | 生成循环,永久执行测试 | | +| 13 | **-t** | 仅运行以逗号分隔的测试命令列表。 | | +| 14 | **-I** | Idle 模式。仅打开 N 个 idle 连接并等待。 | | + + + + +```bash +# 测试:100个并发连接,100000个请求,检测host为localhost 端口为6379的redis服务器性能 +cd /usr/local/bin +redis-benchmark -h localhost -p 6379 -c 100 -n 100000 +``` + +参考资料:https://www.runoob.com/redis/redis-benchmarks.html + +### 基本数据库常识 + +默认16个数据库,类似数组下标从零开始,初始默认使用零号库 + + + +查看 redis.conf ,里面有默认的配置 + +```sh +# Set the number of databases. The default database is DB 0, you can select +# a different one on a per-connection basis using SELECT where +# dbid is a number between 0 and 'databases'-1 +databases 16 +``` + +Select命令切换数据库 + +```sh +127.0.0.1:6379> select 7 +OK +127.0.0.1:6379[7]> +# 不同的库可以存不同的数据 +``` + +Dbsize查看当前数据库的key的数量 + +```bash +127.0.0.1:6379> select 7 +OK +127.0.0.1:6379[7]> DBSIZE +(integer) 0 +127.0.0.1:6379[7]> select 0 +OK +127.0.0.1:6379> DBSIZE +(integer) 5 +127.0.0.1:6379> keys * # 查看具体的key +1) "counter:__rand_int__" +2) "mylist" +3) "k1" +4) "myset:__rand_int__" +5) "key:__rand_int__" +``` + +Flushdb:清空当前库 + +Flushall:清空全部的库 + +```bash +127.0.0.1:6379> DBSIZE +(integer) 5 +127.0.0.1:6379> FLUSHDB +OK +127.0.0.1:6379> DBSIZE +(integer) 0 +``` + +## 关于redis的单线程 + +注:6.x版本有多线程,一般用不到,单线程足够应对 + +我们首先要明白,Redis很快!官方表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就 顺理成章地采用单线程的方案了! + +Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。这个数据不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差! + +**Redis为什么这么快?** + +redis 核心就是 如果我的数据全都在内存里,我单线程的去操作 就是效率最高的,为什么呢,因为 多线程的本质就是 CPU 模拟出来多个线程的情况,这种模拟出来的情况就有一个代价,就是上下文的切 换,对于一个内存的系统来说,它没有上下文的切换就是效率最高的。redis 用 单个CPU 绑定一块内存 的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的,所以它是单线程处 理这个事。在内存的情况下,这个方案就是最佳方案。 + +因为一次CPU上下文的切换大概在 1500ns 左右。从内存中读取 1MB 的连续数据,耗时大约为 250us, 假设1MB的数据由多个线程读取了1000次,那么就有1000次时间上下文的切换,那么就有1500ns * 1000 = 1500us ,我单线程的读完1MB数据才250us ,你光时间上下文的切换就用了1500us了,我还不算你每次读一点数据的时间。 diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/02.\344\272\224\345\244\247\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/02.\344\272\224\345\244\247\346\225\260\346\215\256\347\261\273\345\236\213.md" new file mode 100644 index 00000000..24acd6a2 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/02.\344\272\224\345\244\247\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -0,0 +1,818 @@ +--- +title: 五大数据类型 +date: 2021-05-17 16:11:13 +permalink: /redis/study-note/2/ +--- +## 五大数据类型 + +官方文档: + +![image-20210407112615913](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210407112615913.png) + +全段翻译: + +Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 [字符串(strings)](http://www.redis.cn/topics/data-types-intro.html#strings), [散列(hashes)](http://www.redis.cn/topics/data-types-intro.html#hashes), [列表(lists)](http://www.redis.cn/topics/data-types-intro.html#lists), [集合(sets)](http://www.redis.cn/topics/data-types-intro.html#sets), [有序集合(sorted sets)](http://www.redis.cn/topics/data-types-intro.html#sorted-sets) 与范围查询, [bitmaps](http://www.redis.cn/topics/data-types-intro.html#bitmaps), [hyperloglogs](http://www.redis.cn/topics/data-types-intro.html#hyperloglogs) 和 [地理空间(geospatial)](http://www.redis.cn/commands/geoadd.html) 索引半径查询。 Redis 内置了 [复制(replication)](http://www.redis.cn/topics/replication.html),[LUA脚本(Lua scripting)](http://www.redis.cn/commands/eval.html), [LRU驱动事件(LRU eviction)](http://www.redis.cn/topics/lru-cache.html),[事务(transactions)](http://www.redis.cn/topics/transactions.html) 和不同级别的 [磁盘持久化(persistence)](http://www.redis.cn/topics/persistence.html), 并通过 [Redis哨兵(Sentinel)](http://www.redis.cn/topics/sentinel.html)和自动 [分区(Cluster)](http://www.redis.cn/topics/cluster-tutorial.html)提供高可用性(high availability)。 + +- String (字符串类型) + + String是redis最基本的类型,你可以理解成Memcached一模一样的类型,一个key对应一个value。 + + String类型是二进制安全的,意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象。 + + String类型是redis最基本的数据类型,一个redis中字符串value最多可以是512M。 + +- Hash(哈希,类似 Java里的Map) + + Redis hash 是一个键值对集合。 + + Redis hash 是一个String类型的field和value的映射表,hash特别适合用于存储对象。 + + 类似Java里面的Map + +- List(列表) + + Redis列表是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边)。它的底层实际是个链表 ! + +- Set(集合) + + Redis的Set是String类型的无序集合,它是通过HashTable实现的 ! + +- Zset(sorted set:有序集合) + + Redis zset 和 set 一样,也是String类型元素的集合,且不允许重复的成员。 + + 不同的是每个元素都会关联一个double类型的分数。 + + Redis正是通过分数来为集合中的成员进行从小到大的排序,zset的成员是唯一的,但是分数(Score) 却可以重复。 + +### Redis键(key) + +**字母大写小写都一样** + +- keys * + + 查看所有的key + + ```bash + 127.0.0.1:6379> keys * + (empty list or set) + 127.0.0.1:6379> set name zhiyuan + OK + 127.0.0.1:6379> keys * + 1) "name" + ``` + +- exists key + + 判断某个key是否存在 + + ```bash + 127.0.0.1:6379> EXISTS name + (integer) 1 + 127.0.0.1:6379> EXISTS name1 + (integer) 0 + ``` + +- move key db + + 移动key到别的库 + + ```bash + 127.0.0.1:6379> set name zhiyuan + OK + 127.0.0.1:6379> get name + "zhiyuan" + 127.0.0.1:6379> move name 1 #自动到1库 + (integer) 1 + 127.0.0.1:6379> keys * #在本库查不到name + (empty array) + 127.0.0.1:6379> select 1 #选择1库 + OK + 127.0.0.1:6379[1]> keys * #查询到name + 1) "name" + ``` + +- del key + + 删除key + + ```bash + 127.0.0.1:6379[1]> del name + (integer) 1 + 127.0.0.1:6379[1]> keys * + (empty array) + ``` + +- expire key 秒钟 + + 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删 除。 + + - ttl key + + 查看还有多少秒过期,-1 表示永不过期,-2 表示已过期 + + ```bash + 127.0.0.1:6379> set name zhiyuan + OK + 127.0.0.1:6379> EXPIRE name 10 + (integer) 1 + 127.0.0.1:6379> ttl name + (integer) 4 + 127.0.0.1:6379> ttl name + (integer) 1 + 127.0.0.1:6379> ttl name + (integer) -2 + 127.0.0.1:6379> keys * + (empty list or set) + ``` + +- type key + + 查看你的key是什么类型 + + ```bash + 127.0.0.1:6379> set name zhiyuan + OK + 127.0.0.1:6379> get name + "zhiyuan" + 127.0.0.1:6379> type name + string + ``` + + + +### 字符串String + +**单值多Value** + +set、get、del + +exists(是否存在)、append(追加)、strlen(获取长度) + +```bash +127.0.0.1:6379> set key1 value1 # 设置值 +OK +127.0.0.1:6379> get key1 # 获得key +"value1" +127.0.0.1:6379> del key1 # 删除key +(integer) 1 +127.0.0.1:6379> keys * # 查看全部的key +(empty list or set) +127.0.0.1:6379> exists key1 # 确保 key1 不存在 +(integer) 0 +127.0.0.1:6379> append key1 "hello" # 对不存在的 key进行APPEND,等同于SET key1 "hello" +(integer) 5 # 字符长度 +127.0.0.1:6379> APPEND key1 "-2333" # 对已存在的字符串进行 APPEND +(integer) 10 # 长度从 5 个字符增加到 10 个字符 +127.0.0.1:6379> get key1 +"hello-2333" +127.0.0.1:6379> STRLEN key1 # # 获取字符串的长度 +(integer) 10 +``` + +incr、decr 一定要是数字才能进行加减,+1 和 -1。 + +incrby、decrby 将 key 中储存的数字加上或减去指定的数量。 + +```bash +127.0.0.1:6379> set views 0 # 设置浏览量为0 +OK +127.0.0.1:6379> incr views # 浏览 + 1 +(integer) 1 +127.0.0.1:6379> incr views # 浏览 + 1 +(integer) 2 +127.0.0.1:6379> decr views # 浏览 - 1 +(integer) 1 + +127.0.0.1:6379> incrby views 10 # +10 +(integer) 11 +127.0.0.1:6379> decrby views 10 # -10 +(integer) 1 +``` + +range [范围] + +getrange 获取指定区间范围内的值,类似between...and的关系,从零到负一表示全部 + +```bash +127.0.0.1:6379> set key2 abcd123456 # 设置key2的值 +OK +127.0.0.1:6379> getrange key2 0 -1 # 获得全部的值 +"abcd123456" +127.0.0.1:6379> getrange key2 0 2 # 截取部分字符串 +"abc" +``` + +setrange 设置指定区间范围内的值,格式是setrange key值 具体值 + +```bash +127.0.0.1:6379> get key2 +"abcd123456" +127.0.0.1:6379> SETRANGE key2 1 xx # 替换值 +(integer) 10 +127.0.0.1:6379> get key2 +"axxd123456" +``` + +setex(set with expire)设置过期时间 + +setnx(set if not exist)如何key存在则不覆盖值,还是原来的值(分布式中常用) + +```bash +127.0.0.1:6379> setex key3 60 expire # 设置过期时间 +OK +127.0.0.1:6379> ttl key3 # 查看剩余的时间 +(integer) 55 + +127.0.0.1:6379> setnx mykey "redis" # 如果不存在就设置,成功返回1 +(integer) 1 +127.0.0.1:6379> setnx mykey "mongodb" # 如果值存在则不覆盖值,返回0 +(integer) 0 +127.0.0.1:6379> get mykey +"redis" +``` + +mset:同时设置一个或多个 key-value 对。 + +mget:返回所有(一个或多个) key 的值。 如果给定的 key 里面,有某个 key 不存在,则此 key 返回特殊值nil + +msetnx:当所有 key 都成功设置,返回 1 。如果所有给定 key 都设置失败(至少有一个 key 已经存在),那么返回 0 。相当于原子性操作,要么都成功,要么都不成功。 + +```bash +127.0.0.1:6379> mset k10 v10 k11 v11 k12 v12 +OK +127.0.0.1:6379> keys * +1) "k12" +2) "k11" +3) "k10" + +127.0.0.1:6379> mget k10 k11 k12 k13 +1) "v10" +2) "v11" +3) "v12" +4) (nil) + +127.0.0.1:6379> msetnx k10 v10 k15 v15 # 原子性操作! +(integer) 0 +127.0.0.1:6379> get key15 +(nil) +``` + +存储对象: + +set user:1 value(json数据) + +```bash +127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2 +OK +127.0.0.1:6379> mget user:1:name user:1:age +1) "zhangsan" +2) "2" +``` + +getset:先get再set + +```bash +127.0.0.1:6379> getset db mongodb # 没有旧值,返回 nil +(nil) +127.0.0.1:6379> get db +"mongodb" +127.0.0.1:6379> getset db redis # 返回旧值 mongodb +"mongodb" +127.0.0.1:6379> get db +"redis" +``` + +String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 + +常规计数:微博数,粉丝数等。 + +### 列表List + +**单值多Value** + +可插入重读的值 + +Lpush:将一个或多个值插入到列表头部。(LeftPush左) + +Rpush:将一个或多个值插入到列表尾部。(RightPush右) + +lrange:返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 + +​ 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 + +​ 使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 + +```bash +127.0.0.1:6379> LPUSH list "one" +(integer) 1 +127.0.0.1:6379> LPUSH list "two" +(integer) 2 +127.0.0.1:6379> RPUSH list "right" +(integer) 3 + +127.0.0.1:6379> Lrange list 0 -1 +1) "two" +2) "one" +3) "right" +127.0.0.1:6379> Lrange list 0 1 +1) "two" +2) "one" +``` + +lpop 命令用于移除并返回列表的第一个元素。当列表 key 不存在时,返回 nil 。 + +rpop 移除列表的最后一个元素,返回值为移除的元素。 + +```bash +127.0.0.1:6379> Lpop list +"two" +127.0.0.1:6379> Rpop list +"right" +127.0.0.1:6379> Lrange list 0 -1 +1) "one" +``` + +Lindex,按照索引下标获得元素(-1代表最后一个,0代表是第一个) + +```bash +127.0.0.1:6379> Lindex list 1 +(nil) +127.0.0.1:6379> Lindex list 0 +"one" +127.0.0.1:6379> Lindex list -1 +"one" +``` + +llen 用于返回列表的长度。 + +```bash +127.0.0.1:6379> flushdb +OK +127.0.0.1:6379> Lpush list "one" +(integer) 1 +127.0.0.1:6379> Lpush list "two" +(integer) 2 +127.0.0.1:6379> Lpush list "three" +(integer) 3 +127.0.0.1:6379> Llen list # 返回列表的长度 +(integer) 3 +``` + + lrem (lrem key count element)根据参数 COUNT 的值,移除列表中与参数 VALUE 相等的元素。 + +如果有多个一样的lement,则删除列表最前面的的 + +```bash +127.0.0.1:6379> lrem list 1 "two" +(integer) 1 +127.0.0.1:6379> Lrange list 0 -1 +1) "three" +2) "one" +``` + +Ltrim key 对一个列表进行修剪(trim),只保留指定列表中区间内的元素,不在指定区间之内的元素都将被删除。 + +```bash +127.0.0.1:6379> RPUSH mylist "hello" "hello" "hello2" "hello3" +(integer) 4 +127.0.0.1:6379> ltrim mylist 1 2 +OK +127.0.0.1:6379> lrange mylist 0 -1 +1) "hello" +2) "hello2" +``` + +rpoplpush 移除列表的最后一个元素,并将该元素添加到另一个列表并返回。 + +```bash +127.0.0.1:6379> rpush mylist "hello" +(integer) 1 +127.0.0.1:6379> rpush mylist "foo" +(integer) 2 +127.0.0.1:6379> rpush mylist "bar" +(integer) 3 +127.0.0.1:6379> rpoplpush mylist myotherlist +"bar" +127.0.0.1:6379> lrange mylist 0 -1 +1) "hello" +2) "foo" +127.0.0.1:6379> lrange myotherlist 0 -1 +1) "bar" +``` + +lset key index value ,将列表 key 下标为 index 的元素的值设置为 value 。 + +```bash +127.0.0.1:6379> exists list # 对空列表(key 不存在)进行 LSET +(integer) 0 +127.0.0.1:6379> lset list 0 item # 报错 +(error) ERR no such key + +127.0.0.1:6379> lpush list "value1" # 对非空列表进行 LSET +(integer) 1 +127.0.0.1:6379> lrange list 0 0 +1) "value1" +127.0.0.1:6379> lset list 0 "new" # 更新值 +OK +127.0.0.1:6379> lrange list 0 0 +1) "new" +127.0.0.1:6379> lset list 1 "new" # index 超出范围报错 +(error) ERR index out of range +``` + +linsert key before/after pivot value,用于在列表的元素前或者后插入元素。 + +将值 value 插入到列表 key 当中,位于值 pivot 之前或之后。 + +如果pivot有多个,则插入最前面的那个 + +```bash +127.0.0.1:6379> RPUSH mylist "Hello" +(integer) 1 +127.0.0.1:6379> RPUSH mylist "world" +(integer) 2 +127.0.0.1:6379> lrange mylist 0 -1 +1) "Hello" +2) "world" + +127.0.0.1:6379> LINSERT mylist BEFORE "world" "There" +(integer) 3 +127.0.0.1:6379> lrange mylist 0 -1 +1) "Hello" +2) "There" +3) "world" +``` + + + +**性能总结:** + +- 它是一个字符串链表,left,right 都可以插入添加 +- 如果键不存在,创建新的链表 +- 如果键已存在,新增内容 +- 如果值全移除,对应的键也就消失了 +- 链表的操作无论是头和尾效率都极高,但假如是对中间元素进行操作,效率就很惨淡了。 + +list就是链表,略有数据结构知识的人都应该能理解其结构。使用Lists结构,我们可以轻松地实现最新消息排行等功能。List的另一个应用就是消息队列,可以利用List的PUSH操作,将任务存在List中,然后工作线程再用POP操作将任务取出进行执行。Redis还提供了操作List中某一段的api,你可以直接查询,删除List中某一段的元素。 + +Redis的list是每个子元素都是String类型的双向链表,可以通过push和pop操作从列表的头部或者尾部添加或者删除元素,这样List即可以作为栈,也可以作为队列。 + +### 集合Set + +**单值多value** + + + +无序不重复集合 + +sadd 将一个或多个成员元素加入到集合中,不能重复 + +smembers 返回集合中的所有的成员。 + +sismember 命令判断成员元素是否是集合的成员。 + +```bash +127.0.0.1:6379> sadd myset "hello" +(integer) 1 +127.0.0.1:6379> sadd myset "zhiyuan" +(integer) 1 +127.0.0.1:6379> sadd myset "zhiyuan" # 重复值不插入 返回0 +(integer) 0 +127.0.0.1:6379> SMEMBERS myset #查看集合中所有成员 +1) "zhiyuan" +2) "hello" +127.0.0.1:6379> SISMEMBER myset "hello" #是否是此集合的成员 是反正1 +(integer) 1 +127.0.0.1:6379> SISMEMBER myset "world" +(integer) 0 +``` + +scard,获取集合里面的元素个数 + +```bash +127.0.0.1:6379> scard myset +(integer) 2 +``` + +srem key value 用于移除集合中的一个或多个成员元素 + +```bash +127.0.0.1:6379> srem myset "zhiyuan" +(integer) 1 +127.0.0.1:6379> SMEMBERS myset +1) "hello" +``` + +srandmember key 用于返回集合中随机元素。后面加上数字,则随机返回对应数量的成员,默认一个 + +```bash +127.0.0.1:6379> SMEMBERS myset +1) "zhiyuan" +2) "world" +3) "hello" +127.0.0.1:6379> SRANDMEMBER myset +"hello" +127.0.0.1:6379> SRANDMEMBER myset 2 +1) "world" +2) "zhiyuan" +127.0.0.1:6379> SRANDMEMBER myset 2 +1) "zhiyuan" +2) "hello" +``` + +spop key [count] 用于移除指定 key 集合的随机元素,不填则默认一个。 + +```bash +127.0.0.1:6379> SMEMBERS myset +1) "zhiyuan" +2) "world" +3) "hello" +127.0.0.1:6379> spop myset +"world" +127.0.0.1:6379> spop myset 2 +1) "zhiyuan" +2) "hello" +``` + + smove SOURCE DESTINATION MEMBER, 将指定成员 member 元素从 source 集合移动到 destination 集合 + +```bash +127.0.0.1:6379> sadd myset "hello" #myset 添加元素 +(integer) 1 +127.0.0.1:6379> sadd myset "world" +(integer) 1 +127.0.0.1:6379> sadd myset "zhiyuan" +(integer) 1 +127.0.0.1:6379> sadd myset2 "set2" #myset2 添加元素 +(integer) 1 +127.0.0.1:6379> smove myset myset2 "zhiyuan" +(integer) 1 +127.0.0.1:6379> SMEMBERS myset +1) "world" +2) "hello" +127.0.0.1:6379> SMEMBERS myset2 +1) "zhiyuan" +2) "set2" +``` + +数字集合类: + +- 差集: sdiff +- 交集: sinter +- 并集: sunion + +```bash +127.0.0.1:6379> sadd key1 "a" # key1 +(integer) 1 +127.0.0.1:6379> sadd key1 "b" +(integer) 1 +127.0.0.1:6379> sadd key1 "c" +(integer) 1 +127.0.0.1:6379> sadd key2 "c" # key2 +(integer) 1 +127.0.0.1:6379> sadd key2 "d" +(integer) 1 +127.0.0.1:6379> sadd key2 "e" +(integer) 1 +127.0.0.1:6379> SDIFF key1 key2 # 差集 +1) "a" +2) "b" +127.0.0.1:6379> SINTER key1 key2 # 交集 +1) "c" +127.0.0.1:6379> SUNION key1 key2 # 并集 +1) "a" +2) "b" +3) "c" +4) "e" +5) "d" +``` + +在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。 + +### 哈希Hash + +**k-v模式不变,但V是一个键值对** + +hset、hget 命令用于为哈希表中的字段赋值 + +hmset、hmget 同时将多个field-value对设置到哈希表中。会覆盖哈希表中已存在的字段 + +> Redis 4.0.0开始弃用HMSET,请使用HSET + +hgetall 用于返回哈希表中,所有的字段和值。 + +hdel 用于删除哈希表 key 中的一个或多个指定字段 + +```bash +127.0.0.1:6379> hset myhash field1 "zhiyuan" +(integer) 1 +127.0.0.1:6379> hget myhash field1 +"zhiyuan" + +127.0.0.1:6379> HSET myhash field1 "Hello" field2 "World" +(integer) 2 +127.0.0.1:6379> hgetall myhash +1) "field1" +2) "Hello" +3) "field2" +4) "World" +127.0.0.1:6379> HGET myhash field1 +"Hello" +127.0.0.1:6379> HGET myhash field2 +"World" + +127.0.0.1:6379> HDEL myhash field1 +(integer) 1 +127.0.0.1:6379> hgetall myhash +1) "field2" +2) "World" + +``` + +hlen 获取哈希表中字段的数量 + +```bash +127.0.0.1:6379> hlen myhash +(integer) 1 +127.0.0.1:6379> HSET myhash field1 "Hello" field2 "World" +OK +127.0.0.1:6379> hlen myhash +(integer) 2 +``` + +hexists 查看哈希表的指定字段是否存在 + +```bash +127.0.0.1:6379> hexists myhash field1 +(integer) 1 +127.0.0.1:6379> hexists myhash field3 +(integer) 0 +``` + +hkeys 获取哈希表中的所有域(field) + +hvals 返回哈希表所有域(field)的值 + +```bash +127.0.0.1:6379> HKEYS myhash +1) "field2" +2) "field1" +127.0.0.1:6379> HVALS myhash +1) "World" +2) "Hello" +``` + +hincrby 为哈希表中的字段值加上指定增量值 + +```bash +127.0.0.1:6379> hset myhash field 5 +(integer) 1 +127.0.0.1:6379> HINCRBY myhash field 1 +(integer) 6 +127.0.0.1:6379> HINCRBY myhash field -1 +(integer) 5 +127.0.0.1:6379> HINCRBY myhash field -10 +(integer) -5 +``` + +hsetnx 为哈希表中不存在的的字段赋值 ,存在则不赋值 + +```bash +127.0.0.1:6379> HSETNX myhash field1 "hello" +(integer) 1 # 设置成功,返回 1 。 +127.0.0.1:6379> HSETNX myhash field1 "world" +(integer) 0 # 如果给定字段已经存在,返回 0 。 +127.0.0.1:6379> HGET myhash field1 +"hello" +``` + +Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。 存储部分变更的数据,如用户信息等。 + +### 有序集合Zset + +在set基础上,加一个score值。之前set是k1 v1 v2 v3,现在zset是 k1 score1 v1 score2 v2 + + + +zadd 将一个或多个成员元素及其分数值加入到有序集当中。 + +zrange 返回有序集中,指定区间内的成员 + +```bash +127.0.0.1:6379> zadd myset 1 "one" +(integer) 1 +127.0.0.1:6379> zadd myset 2 "two" 3 "three" +(integer) 2 +127.0.0.1:6379> ZRANGE myset 0 -1 +1) "one" +2) "two" +3) "three" +``` + +zrangebyscore 返回有序集合中指定分数区间的成员列表。有序集成员按分数值递增(从小到大) 次序排列。 + +ZREVRANGE 从大到小 + +```bash +127.0.0.1:6379> zadd salary 2500 xiaoming +(integer) 1 +127.0.0.1:6379> zadd salary 5000 xiaohong +(integer) 1 +127.0.0.1:6379> zadd salary 500 kuangshen +(integer) 1 + +# Inf无穷大量+∞,同样地,-∞可以表示为-Inf。 +127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 显示整个有序集 +1) "zhiyuan" +2) "xiaoming" +3) "xiaohong" +127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 递增排列 +1) "zhiyuan" +2) "500" +3) "xiaoming" +4) "2500" +5) "xiaohong" +6) "5000" +127.0.0.1:6379> ZREVRANGE salary 0 -1 WITHSCORES # 递减排列 +1) "xiaohong" +2) "5000" +3) "xiaoming" +4) "2500" +5) "zhiyuan" +6) "500" +# 显示工资 <=2500的所有成员 +127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 WITHSCORES +1) "zhiyuan" +2) "500" +3) "xiaoming" +4) "2500" +``` + +zrem 移除有序集中的一个或多个成员 + +```bash +127.0.0.1:6379> ZRANGE salary 0 -1 +1) "zhiyuan" +2) "xiaoming" +3) "xiaohong" +127.0.0.1:6379> zrem salary zhiyuan +(integer) 1 +127.0.0.1:6379> ZRANGE salary 0 -1 +1) "xiaoming" +2) "xiaohong" +``` + +zcard 命令用于计算集合中元素的数量 + +```bash +127.0.0.1:6379> zcard salary +(integer) 2 +OK +``` + +zcount 计算有序集合中指定分数区间的成员数量。 + +```bash +127.0.0.1:6379> zadd myset 1 "hello" +(integer) 1 +127.0.0.1:6379> zadd myset 2 "world" 3 "zhiyuan" +(integer) 2 +127.0.0.1:6379> ZCOUNT myset 1 3 +(integer) 3 +127.0.0.1:6379> ZCOUNT myset 1 2 +(integer) 2 +``` + +zrank 返回有序集中指定成员的**排名**。其中有序集成员按分数值递增(从小到大)顺序排列。 + +```bash +127.0.0.1:6379> zadd salary 2500 xiaoming +(integer) 1 +127.0.0.1:6379> zadd salary 5000 xiaohong +(integer) 1 +127.0.0.1:6379> zadd salary 500 kuangshen +(integer) 1 +127.0.0.1:6379> ZRANGE salary 0 -1 WITHSCORES # 显示所有成员及其 score 值 +1) "kuangshen" +2) "500" +3) "xiaoming" +4) "2500" +5) "xiaohong" +6) "5000" +127.0.0.1:6379> zrank salary kuangshen # 显示 kuangshen 的薪水排名,最少 +(integer) 0 +127.0.0.1:6379> zrank salary xiaohong # 显示 xiaohong 的薪水排名,第三 +(integer) 2 +``` + +zrevrank 返回有序集中成员的排名。其中有序集成员按分数值递减(从大到小)排序 + +```bash +127.0.0.1:6379> ZREVRANK salary kuangshen # 狂神第三 +(integer) 2 +127.0.0.1:6379> ZREVRANK salary xiaohong # 小红第一 +(integer) 0 +``` + +和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列,比如 一个存储全班同学成绩的sorted set,其集合value可以是同学的学号,而score就可以是其考试得分, 这样在数据插入集合的时候,就已经进行了天然的排序。可以用sorted set来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让 重要的任务优先执行。 diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/03.\344\270\211\347\247\215\347\211\271\346\256\212\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/03.\344\270\211\347\247\215\347\211\271\346\256\212\346\225\260\346\215\256\347\261\273\345\236\213.md" new file mode 100644 index 00000000..dc032c63 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/03.\344\270\211\347\247\215\347\211\271\346\256\212\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -0,0 +1,315 @@ +--- +title: 三种特殊数据类型 +date: 2021-05-17 16:12:21 +permalink: /redis/study-note/3/ +--- +## 三种特殊数据类型 + +### GEO地理位置 + +#### 简介 + +Redis 的 GEO 特性在 Redis 3.2 版本中推出, 这个功能可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作。来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。geo的数据类型为 zset。 + +GEO 的数据结构总共有六个常用命令:geoadd、geopos、geodist、georadius、 georadiusbymember、gethash + +官方文档:https://www.redis.net.cn/order/3685.html + +#### 常用指令 + +**1、geoadd** + +语法:geoadd key longitude latitude member ... + +将给定的空间元素(经度、纬度、名字)添加到指定的键里面。 +这些数据会以有序集的形式被储存在键里面,从而使得georadius和georadiusbymember这样的 +命令可以在之后通过位置查询取得这些元素。 +geoadd命令以标准的x,y格式接受参数,所以用户必须先输入经度,然后再输入纬度。 +geoadd能够记录的坐标是有限的:非常接近两极的区域无法被索引。 +有效的经度介于-180-180度之间,有效的纬度介于-85.05112878 度至 85.05112878 度之间 +当用户尝试输入一个超出范围的经度或者纬度时,geoadd命令将返回一个错误。 + +测试:百度搜索经纬度查询,模拟真实数据 + +```bash +127.0.0.1:6379> geoadd china:city 116.23 40.22 北京 +(integer) 1 +127.0.0.1:6379> geoadd china:city 121.48 31.40 上海 113.88 22.55 深圳 120.21 30.20 杭州 +(integer) 3 +127.0.0.1:6379> geoadd china:city 106.54 29.40 重庆 108.93 34.23 西安 114.02 30.58 武汉 +(integer) 3 +``` + +**2、geopos** + +语法:geopos key member [member...] + +从key里返回所有给定位置元素的位置(经度和纬度) + +```bash +127.0.0.1:6379> geopos china:city 北京 +1) 1) "116.23000055551528931" +2) "40.2200010338739844" +127.0.0.1:6379> geopos china:city 上海 重庆 +1) 1) "121.48000091314315796" +2) "31.40000025319353938" +2) 1) "106.54000014066696167" +2) "29.39999880018641676" +127.0.0.1:6379> geopos china:city 新疆 +1) (nil) +``` + +**3、geodist** + +语法:geodist key member1 member2 [unit] + +返回两个给定位置之间的距离,如果两个位置之间的其中一个不存在,那么命令返回空值 + +指定单位的参数unit必须是以下单位的其中一个: + +- m表示单位为米 +- km表示单位为千米 +- mi表示单位为英里 +- ft表示单位为英尺 + +如果用户没有显式地指定单位参数,那么geodist默认使用 米 作为单位。 + +geodist命令在计算距离时会假设地球为完美的球形,在极限情况下,这一假设最大会造成0.5%的误差。 + +```bash +127.0.0.1:6379> geodist china:city 北京 上海 +"1088785.4302" +127.0.0.1:6379> geodist china:city 北京 上海 km +"1088.7854" +127.0.0.1:6379> geodist china:city 重庆 北京 km +"1491.6716" +``` + +**4、georadius** + +语法: + +```bash +georadius key longitude latitude radius m|km|ft|mi [withcoord][withdist] [withhash][asc|desc][count count] +``` + +以给定的经纬度为中心, 找出某一半径内的元素 + +重新连接 redis-cli,增加参数 --raw ,可以强制输出中文,不然会乱码 + +```bash +127.0.0.1:6379> georadius china:city 100 30 1000 km #乱码 +1) "\xe9\x87\x8d\xe5\xba\x86" +2) "\xe8\xa5\xbf\xe5\xae\x89" +127.0.0.1:6379> exit +[root@localhost bin]# redis-cli --raw -p 6379 +# 在 china:city 中寻找坐标 100 30 半径为 1000km 的城市 +127.0.0.1:6379> georadius china:city 100 30 1000 km +重庆 +西安 + +``` + + withdist 返回位置名称和中心距离 + +```bash +127.0.0.1:6379> georadius china:city 100 30 1000 km withdist +重庆 +635.2850 +西安 +963.3171 +``` + +withcoord 返回位置名称、经纬度 + +```bash +127.0.0.1:6379> georadius china:city 100 30 1000 km withcoord +重庆 +106.54000014066696167 +29.39999880018641676 +西安 +108.92999857664108276 +34.23000121926852302 +``` + +withdist withcoord 返回位置名称、距离、经纬度,count 限定寻找个数 + +```bash +127.0.0.1:6379> georadius china:city 100 30 1000 km withcoord withdist count 1 +重庆 +635.2850 +106.54000014066696167 +29.39999880018641676 +127.0.0.1:6379> georadius china:city 100 30 1000 km withcoord withdist count 2 +重庆 +635.2850 +106.54000014066696167 +29.39999880018641676 +西安 +963.3171 +108.92999857664108276 +34.23000121926852302 +``` + +**5、georadiusbymember** + +语法: + +```bash +georadiusbymember key member radius m|km|ft|mi [withcoord][withdist] [withhash][asc|desc][count count] +``` + +找出位于指定范围内的元素,中心点是由给定的位置元素决定 + +```bash +127.0.0.1:6379> GEORADIUSBYMEMBER china:city 北京 1000 km +北京 +西安 +127.0.0.1:6379> GEORADIUSBYMEMBER china:city 上海 400 km +杭州 +上海 +``` + +**6、geohash** + +语法:geohash key member [member...] + +Redis使用geohash将二维经纬度转换为一维字符串,字符串越长表示位置更精确,两个字符串越相似表示距离越近。 + +```bash +127.0.0.1:6379> geohash china:city 北京 重庆 +wx4sucu47r0 +wm5z22h53v0 +127.0.0.1:6379> geohash china:city 北京 上海 +wx4sucu47r0 +wtw6sk5n300 +``` + +**zrem** + +GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除。 + +```bash +127.0.0.1:6379> geoadd china:city 116.23 40.22 beijing +1 +127.0.0.1:6379> zrange china:city 0 -1 # 查看全部的元素 +重庆 +西安 +深圳 +武汉 +杭州 +上海 +beijing +北京 +127.0.0.1:6379> zrem china:city beijing # 移除元素 +1 +127.0.0.1:6379> zrem china:city 北京 # 移除元素 +1 +127.0.0.1:6379> zrange china:city 0 -1 +重庆 +西安 +深圳 +武汉 +杭州 +上海 +``` + +### HyperLogLog + +Redis 在 2.8.9 版本添加了 HyperLogLog 结构。 + +Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。 + +在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。 + +HyperLogLog则是一种算法,它提供了不精确的去重计数方案。 + +举个栗子:假如我要统计网页的UV(浏览用户数量,一天内同一个用户多次访问只能算一次),传统的解决方案是使用Set来保存用户id,然后统计Set中的元素数量来获取页面UV。但这种方案只能承载少量用户,一旦用户数量大起来就需要消耗大量的空间来存储用户id。我的目的是统计用户数量而不是保存用户,这简直是个吃力不讨好的方案!而使用Redis的HyperLogLog最多需要12k就可以统计大量的用户数,尽管它大概有0.81%的错误率,但对于统计UV这种不需要很精确的数据是可以忽略不计的。 + + + +**什么是基数?** + +比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素的数量)为5。 + +基数估计就是在误差可接受的范围内,快速计算基数。 + + + +**基本命令** + +| 序号 | 命令及描述 | +| :--- | :----------------------------------------------------------- | +| 1 | [PFADD key element [element ...\]](https://www.runoob.com/redis/hyperloglog-pfadd.html)
添加指定元素到 HyperLogLog 中。 | +| 2 | [PFCOUNT key [key ...\]](https://www.runoob.com/redis/hyperloglog-pfcount.html)
返回给定 HyperLogLog 的基数估算值。 | +| 3 | [PFMERGE destkey sourcekey [sourcekey ...\]](https://www.runoob.com/redis/hyperloglog-pfmerge.html)
将多个 HyperLogLog 合并为一个 HyperLogLog,并集计算 | + +```bash +127.0.0.1:6379> PFADD mykey a b c d e f g h i j +1 +127.0.0.1:6379> PFCOUNT mykey +10 +127.0.0.1:6379> PFADD mykey2 i j z x c v b n m +1 +127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 +OK +127.0.0.1:6379> PFCOUNT mykey3 +15 +``` + +### BitMap + +#### 简介 + +在开发中,可能会遇到这种情况:需要统计用户的某些信息,如活跃或不活跃,登录或者不登录;又如 需要记录用户一年的打卡情况,打卡了是1, 没有打卡是0,如果使用普通的 key/value存储,则要记录 365条记录,如果用户量很大,需要的空间也会很大,所以 Redis 提供了 Bitmap 位图这中数据结构, Bitmap 就是通过操作二进制位来进行记录,即为 0 和 1;如果要记录 365 天的打卡情况,使用 Bitmap 表示的形式大概如下:0101000111000111...........................,这样有什么好处呢?当然就是节约内存 了,365 天相当于 365 bit,又 1 字节 = 8 bit , 所以相当于使用 46 个字节即可。 + +BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,实际上底层也是通过对字符串的操作来实现。Redis 从 2.2 版本之后新增了setbit, getbit, bitcount 等几个 bitmap 相关命令。 + +#### **操作** + +**1、setbit 设置操作** + +SETBIT key offset value : 设置 key 的第 offset 位为value (1或0) + +使用 bitmap 来记录上述事例中一周的打卡记录如下所示: + +周一:1,周二:0,周三:0,周四:1,周五:1,周六:0,周天:0 (1 为打卡,0 为不打卡) + +```bash +127.0.0.1:6379> setbit sign 0 1 +0 +127.0.0.1:6379> setbit sign 1 0 +0 +127.0.0.1:6379> setbit sign 2 0 +0 +127.0.0.1:6379> setbit sign 3 1 +0 +127.0.0.1:6379> setbit sign 4 1 +0 +127.0.0.1:6379> setbit sign 5 0 +0 +127.0.0.1:6379> setbit sign 6 0 +0 +``` + +**2、getbit 获取操作** + +GETBIT key offset 获取offset设置的值,未设置过默认返回0 + +```bash +127.0.0.1:6379> getbit sign 3 # 查看周四是否打卡 +1 +127.0.0.1:6379> getbit sign 6 # 查看周七是否打卡 +0 +``` + +**3、bitcount 统计操作** + +bitcount key [start, end] 统计 key 上位为1的个数 + +统计这周打卡的记录,可以看到只有3天是打卡的状态: + +```bash +127.0.0.1:6379> bitcount sign +3 +``` diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/04.Redis\344\272\213\345\212\241.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/04.Redis\344\272\213\345\212\241.md" new file mode 100644 index 00000000..5a27ab26 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/04.Redis\344\272\213\345\212\241.md" @@ -0,0 +1,150 @@ +--- +title: Redis事务 +date: 2021-05-17 16:13:06 +permalink: /redis/study-note/4/ +--- +## Redis事务 + +### 理论 + +**Redis事务的概念:** + +Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事 务执行命令序列中。 + +总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。 + +**Redis事务没有隔离级别的概念:** + +批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行! + +**Redis不保证原子性:** + +Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其 余的命令仍会被执行。 + +**Redis事务的三个阶段:** + +- 开始事务 +- 命令入队 +- 执行事务 + +Redis事务相关命令: + +| 序号 | 命令及描述 | +| :--- | :----------------------------------------------------------- | +| 1 | DISCARD
取消事务,放弃执行事务块内的所有命令。 | +| 2 | EXEC
执行所有事务块内的命令。 | +| 3 | MULTI
标记一个事务块的开始。 | +| 4 | UNWATCH
取消 WATCH 命令对所有 key 的监视。 | +| 5 | WATCH key [key ...]
监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。( 类似乐观锁 ) | + +### 实践 + +正常执行 + +![image-20210408101936847](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408101936847.png) + +放弃事务 + +![image-20210408101955501](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408101955501.png) + +若在事务队列中存在命令性错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行 + +![image-20210408102023204](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408102023204.png) + +若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常。 + +![image-20210408102051072](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408102051072.png) + +**Watch 监控** + +- 悲观锁: + + 悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿到这个数据就会block直到它拿到锁。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。 + +- 乐观锁: + + 乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会 锁。但是在更新的时候会判断一下再此期间别人有没有去更新这个数据,可以使用版本号等机制,乐观锁适用于多读的应用类型,这样可以提高吞吐量,乐观锁策略:提交版本必须大于记录当前版本才能 执行更新。 + +测试: + +1、初始化信用卡可用余额和欠额 + +```bash +127.0.0.1:6379> set balance 100 +OK +127.0.0.1:6379> set debt 0 +OK +``` + +2、使用watch检测balance,事务期间balance数据未变动,事务执行成功 + +```bash +127.0.0.1:6379> watch balance +OK +127.0.0.1:6379> MULTI #开启事务 +OK +127.0.0.1:6379> decrby balance 20 #可用余额-20 +QUEUED +127.0.0.1:6379> incrby debt 20 #欠款+20 +QUEUED +127.0.0.1:6379> exec #执行事务 +1) (integer) 80 +2) (integer) 20 +``` + +3、使用watch检测balance,若事务期间balance数据变动,事务执行失败! + +```bash +# 窗口一 +127.0.0.1:6379> watch balance #监视balance +OK +127.0.0.1:6379> MULTI # 执行完毕后,执行窗口二代码测试 +OK +127.0.0.1:6379> decrby balance 20 +QUEUED +127.0.0.1:6379> incrby debt 20 +QUEUED +127.0.0.1:6379> exec # 修改失败!因为被监视的balance值改变 +(nil) + +``` + +窗口二 + +```bash +# 窗口二 +127.0.0.1:6379> get balance +"80" +127.0.0.1:6379> set balance 200 +OK +``` + +窗口一:出现问题后放弃监视,然后重来! + +```bash +127.0.0.1:6379> UNWATCH # 放弃监视,这是取消所有的监视 +OK +127.0.0.1:6379> watch balance #监视 +OK +127.0.0.1:6379> MULTI #事务 +OK +127.0.0.1:6379> decrby balance 20 +QUEUED +127.0.0.1:6379> incrby debt 20 +QUEUED +127.0.0.1:6379> exec # 成功! +1) (integer) 180 +2) (integer) 40 +``` + +说明: + +一但执行 EXEC 开启事务的执行后,无论事务使用执行成功, WARCH 对变量的监控都将被取消。 + +故当事务执行失败后,需重新执行WATCH命令对变量进行监控,并开启新的事务进行操作。 + +### 小结 + +watch指令类似于乐观锁,在事务提交时,如果watch监控的多个KEY中任何KEY的值已经被其他客户端更改,则使用EXEC执行事务时,事务队列将不会被执行,同时返回Nullmulti-bulk应答以通知调用者事务执行失败。 + + diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/05.Redis\344\272\216Java.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/05.Redis\344\272\216Java.md" new file mode 100644 index 00000000..6685896f --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/05.Redis\344\272\216Java.md" @@ -0,0 +1,491 @@ +--- +title: Redis于Java +date: 2021-05-17 16:13:40 +permalink: /redis/study-note/5/ +--- +## Jedis + +Jedis是Redis官方推荐的Java连接开发工具。要在Java开发中使用好Redis中间件,必须对Jedis熟悉才能 写成漂亮的代码 + +### 测试ping + +前提打开了redis服务。 + +1、新建一个普通的Maven项目 + +2、导入redis的依赖! + +```xml + + + + redis.clients + jedis + 3.5.2 + + + com.alibaba + fastjson + 1.2.75 + + +``` + +3、编写测试代码 + +```java +public class Ping { + public static void main(String[] args) { + Jedis jedis = new Jedis("127.0.0.1", 6379); + //查看服务是否运行 + System.out.println(jedis.ping()); + } +} +``` + +### 常用API + +基本操作 + +```java +public static void main(String[] args) { + Jedis jedis = new Jedis("127.0.0.1", 6379); + + //验证密码,如果没有设置密码这段代码省略 + // jedis.auth("password"); + + jedis.connect(); //连接 + jedis.disconnect(); //断开连接 + jedis.flushAll(); //清空所有的key +} +``` + +对key操作的命令 + +```java +public class TestKey { + public static void main(String[] args) { + Jedis jedis = new Jedis("127.0.0.1", 6379); + + System.out.println("清空数据:" + jedis.flushDB()); + System.out.println("判断某个键是否存在:" + jedis.exists("username")); + System.out.println("新增<'username','zhiyuan'>的键值对:" + jedis.set("username", "zhiyuan")); + System.out.println("新增<'password','password'>的键值对:" + jedis.set("password", "password")); + + System.out.print("系统中所有的键如下:"); + Set keys = jedis.keys("*"); + System.out.println(keys); + + System.out.println("删除键password:" + jedis.del("password")); + System.out.println("判断键password是否存在:" + jedis.exists("password")); + System.out.println("查看键username所存储的值的类型:" + jedis.type("username")); + System.out.println("随机返回key空间的一个:" + jedis.randomKey()); + System.out.println("重命名key:" + jedis.rename("username", "name")); + System.out.println("取出改后的name:" + jedis.get("name")); + System.out.println("按索引查询:" + jedis.select(0)); + System.out.println("删除当前选择数据库中的所有key:" + jedis.flushDB()); + System.out.println("返回当前数据库中key的数目:" + jedis.dbSize()); + System.out.println("删除所有数据库中的所有key:" + jedis.flushAll()); + } +} +``` + +对String操作的命令 + +```java +public class TestString { + public static void main(String[] args) { + Jedis jedis = new Jedis("127.0.0.1", 6379); + + jedis.flushDB(); + System.out.println("===========增加数据==========="); + System.out.println(jedis.set("key1", "value1")); + System.out.println(jedis.set("key2", "value2")); + System.out.println(jedis.set("key3", "value3")); + + System.out.println("删除键key2:" + jedis.del("key2")); + System.out.println("获取键key2:" + jedis.get("key2")); + System.out.println("修改key1:" + jedis.set("key1", "value1Changed")); + System.out.println("获取key1的值:" + jedis.get("key1")); + System.out.println("在key3后面加入值:" + jedis.append("key3", "End")); + System.out.println("key3的值:" + jedis.get("key3")); + System.out.println("增加多个键值对:" + jedis.mset("key01", "value01", "key02", "value02", "key03", "value03")); + System.out.println("获取多个键值对:" + jedis.mget("key01", "key02", "key03")); + System.out.println("获取多个键值对:" + jedis.mget("key01", "key02", "key03", "key04")); + System.out.println("删除多个键值对:" + jedis.del("key01", "key02")); + System.out.println("获取多个键值对:" + jedis.mget("key01", "key02", "key03")); + + jedis.flushDB(); + + System.out.println("===========新增键值对防止覆盖原先值=============="); + System.out.println(jedis.setnx("key1", "value1")); + System.out.println(jedis.setnx("key2", "value2")); + System.out.println(jedis.setnx("key2", "value2-new")); + System.out.println(jedis.get("key1")); + System.out.println(jedis.get("key2")); + System.out.println("===========新增键值对并设置有效时间============="); + System.out.println(jedis.setex("key3", 2, "value3")); + System.out.println(jedis.get("key3")); + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(jedis.get("key3")); + System.out.println("===========获取原值,更新为新值=========="); + System.out.println(jedis.getSet("key2", "key2GetSet")); + System.out.println(jedis.get("key2")); + System.out.println("获得key2的值的字串:" + jedis.getrange("key2", 2,4)); + + } +} +``` + +对List操作命令 + +```java +public class TestList { + public static void main(String[] args) { + Jedis jedis = new Jedis("127.0.0.1", 6379); + jedis.flushDB(); + System.out.println("===========添加一个list==========="); + jedis.lpush("collections", "ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap"); + jedis.lpush("collections", "HashSet"); + jedis.lpush("collections", "TreeSet"); + jedis.lpush("collections", "TreeMap"); + System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));//-1代表倒数第一个元素,-2代表倒数第二个元素,end为-1表示查询全部 + System.out.println("collections区间0-3的元素:" + jedis.lrange("collections", 0, 3)); + System.out.println("==============================="); + // 删除列表指定的值 ,第二个参数为删除的个数(有重复时),后add进去的值先被删,类似于出栈 + System.out.println("删除指定元素个数:" + jedis.lrem("collections", 2, + "HashMap")); + System.out.println("collections的内容:" + jedis.lrange("collections", + 0, -1)); + System.out.println("删除下表0-3区间之外的元素:" + jedis.ltrim("collections", 0, 3)); + System.out.println("collections的内容:" + jedis.lrange("collections", + 0, -1)); + System.out.println("collections列表出栈(左端):" + jedis.lpop("collections")); + System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1)); + System.out.println("collections添加元素,从列表右端,与lpush相对应:" + jedis.rpush("collections", "EnumMap")); + System.out.println("collections的内容:" + jedis.lrange("collections", + 0, -1)); + System.out.println("collections列表出栈(右端):" + jedis.rpop("collections")); + System.out.println("collections的内容:" + jedis.lrange("collections", + 0, -1)); + System.out.println("修改collections指定下标1的内容:" + jedis.lset("collections", 1, "LinkedArrayList")); + System.out.println("collections的内容:" + jedis.lrange("collections", + 0, -1)); + System.out.println("==============================="); + System.out.println("collections的长度:" + jedis.llen("collections")); + System.out.println("获取collections下标为2的元素:" + jedis.lindex("collections", 2)); + System.out.println("==============================="); + jedis.lpush("sortedList", "3", "6", "2", "0", "7", "4"); + System.out.println("sortedList排序前:" + jedis.lrange("sortedList", 0, + -1)); + System.out.println(jedis.sort("sortedList")); + System.out.println("sortedList排序后:" + jedis.lrange("sortedList", 0, -1)); + + } +} +``` + +对Set的操作命令 + +```java +public class TestSet { + public static void main(String[] args) { + Jedis jedis = new Jedis("127.0.0.1", 6379); + jedis.flushDB(); + System.out.println("============向集合中添加元素(不重复)============"); + System.out.println(jedis.sadd("eleSet", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5")); + System.out.println(jedis.sadd("eleSet", "e6")); + System.out.println(jedis.sadd("eleSet", "e6")); + System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet")); + System.out.println("删除一个元素e0:" + jedis.srem("eleSet", "e0")); + + System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet")); + System.out.println("删除两个元素e7和e6:" + jedis.srem("eleSet", "e7", "e6")); + System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet")); + System.out.println("随机的移除集合中的一个元素:" + jedis.spop("eleSet")); + System.out.println("随机的移除集合中的一个元素:" + jedis.spop("eleSet")); + System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet")); + System.out.println("eleSet中包含元素的个数:" + jedis.scard("eleSet")); + System.out.println("e3是否在eleSet中:" + jedis.sismember("eleSet", "e3")); + System.out.println("e1是否在eleSet中:" + jedis.sismember("eleSet", "e1")); + System.out.println("e1是否在eleSet中:" + jedis.sismember("eleSet", "e5")); + System.out.println("================================="); + System.out.println(jedis.sadd("eleSet1", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5")); + System.out.println(jedis.sadd("eleSet2","e1", "e2", "e4", "e3", "e0", "e8")); + System.out.println("将eleSet1中删除e1并存入eleSet3中:" + jedis.smove("eleSet1", "eleSet3", "e1"));//移到集合元素 + System.out.println("将eleSet1中删除e2并存入eleSet3中:" + jedis.smove("eleSet1", "eleSet3", "e2")); + System.out.println("eleSet1中的元素:" + jedis.smembers("eleSet1")); + System.out.println("eleSet3中的元素:" + jedis.smembers("eleSet3")); + System.out.println("============集合运算================="); + System.out.println("eleSet1中的元素:" + jedis.smembers("eleSet1")); + System.out.println("eleSet2中的元素:" + jedis.smembers("eleSet2")); + System.out.println("eleSet1和eleSet2的交集:" + jedis.sinter("eleSet1", "eleSet2")); + System.out.println("eleSet1和eleSet2的并集:" + jedis.sunion("eleSet1", "eleSet2")); + System.out.println("eleSet1和eleSet2的差集:" + jedis.sdiff("eleSet1", "eleSet2"));//eleSet1中有,eleSet2中没有 + jedis.sinterstore("eleSet4", "eleSet1", "eleSet2");//求交集并将交集保存到dstkey的集合 + System.out.println("eleSet4中的元素:" + jedis.smembers("eleSet4")); + } +} +``` + +对Hash的操作命令 + +```java +public class TestHash { + public static void main(String[] args) { + Jedis jedis = new Jedis("127.0.0.1", 6379); + jedis.flushDB(); + Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key2", "value2"); + map.put("key3", "value3"); + map.put("key4", "value4"); + //添加名称为hash(key)的hash元素 + jedis.hmset("hash", map); + //向名称为hash的hash中添加key为key5,value为value5元素 + jedis.hset("hash", "key5", "value5"); + System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));//return Map + + System.out.println("散列hash的所有键为:" + jedis.hkeys("hash"));//returnSet + System.out.println("散列hash的所有值为:" + jedis.hvals("hash"));//returnList + System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy("hash", "key6", 6)); + System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash")); + System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy("hash", "key6", 3)); + System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash")); + System.out.println("删除一个或者多个键值对:" + jedis.hdel("hash", "key2")); + System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash")); + System.out.println("散列hash中键值对的个数:" + jedis.hlen("hash")); + System.out.println("判断hash中是否存在key2:" + jedis.hexists("hash", "key2")); + System.out.println("判断hash中是否存在key3:" + jedis.hexists("hash", "key3")); + System.out.println("获取hash中的值:" + jedis.hmget("hash", "key3")); + System.out.println("获取hash中的值:" + jedis.hmget("hash", "key3", "key4")); + + } +} +``` + +### 事务 + +```java +public class TestMulti { + public static void main(String[] args) { + //创建客户端连接服务端,redis服务端需要被开启 + Jedis jedis = new Jedis("127.0.0.1", 6379); + jedis.flushDB(); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("hello", "world"); + jsonObject.put("name", "java"); + //开启事务 + Transaction multi = jedis.multi(); + String result = jsonObject.toJSONString(); + try { + //向redis存入一条数据 + multi.set("json", result); + //再存入一条数据 + multi.set("json2", result); + //这里引发了异常,用0作为被除数 + int i = 100 / 0; + //如果没有引发异常,执行进入队列的命令 + multi.exec(); + } catch (Exception e) { + e.printStackTrace(); + //如果出现异常,回滚 + multi.discard(); + } finally { + System.out.println(jedis.get("json")); + System.out.println(jedis.get("json2")); + //最终关闭客户端 + jedis.close(); + } + } +} +``` + +## SpringBoot整合 + +### 基础使用 + +**概述:** + +在SpringBoot中一般使用RedisTemplate提供的方法来操作Redis。那么使用SpringBoot整合Redis需要 那些步骤呢。 + +1、 JedisPoolConfig (这个是配置连接池) + +2、 RedisConnectionFactory 这个是配置连接信息,这里的RedisConnectionFactory是一个接口,我们需要使用它的实现类,在SpringD Data Redis方案中提供了以下四种工厂模型: + +- JredisConnectionFactory +- JedisConnectionFactory +- LettuceConnectionFactory +- SrpConnectionFactory + +3、 RedisTemplate 基本操作 + + + +**导入依赖** + +```xml + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +说明:在springboot2.x之后,原来使用的jedis被替换成lettuce + + + +**yaml配置** + +```yml +spring: + redis: + host: 127.0.0.1 + port: 6379 +``` + +**测试类中测试** + +```java +@Autowired +private RedisTemplate redisTemplate; + +@Test +void contextLoads() { + redisTemplate.opsForValue().set("myKey", "myValue"); + System.out.println(redisTemplate.opsForValue().get("myKey")); +} +``` + + + +### 序列化config + +创建springboot新项目,安装上面步骤导入依赖 + + + +1、分析 RedisAutoConfiguration 自动配置类 + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(RedisOperations.class) +@EnableConfigurationProperties(RedisProperties.class) +@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) +public class RedisAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "redisTemplate") + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + +} +``` + + + +通过源码可以看出,SpringBoot自动帮我们在容器中生成了一个RedisTemplate和一个StringRedisTemplate。 + +但是,这个RedisTemplate的泛型是,写代码不方便,需要写好多类型转换的代码;我们需要一个泛型为形式的RedisTemplate。 + +并且,这个RedisTemplate没有设置数据存在Redis时,key及value的序列化方式。 + +看到这个@ConditionalOnMissingBean注解后,就知道如果Spring容器中有了RedisTemplate对象了, 这个自动配置的RedisTemplate不会实例化。因此我们可以直接自己写个配置类,配置 RedisTemplate。 + +用这个配置我们不可以存储对象,否则会报SerializationException,大家可自己试试 + +2、既然自动配置不好用,就重新配置一个RedisTemplate + +```java +package com.oddfar.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(factory); + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); + ObjectMapper om = new ObjectMapper(); + om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); +// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); //此方法已过期 + //新方法 + om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); + jackson2JsonRedisSerializer.setObjectMapper(om); + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + + // key采用String的序列化方式 + template.setKeySerializer(stringRedisSerializer); + // hash的key也采用String的序列化方式 + template.setHashKeySerializer(stringRedisSerializer); + // value序列化方式采用jackson + template.setValueSerializer(jackson2JsonRedisSerializer); + // hash的value序列化方式采用jackson + template.setHashValueSerializer(jackson2JsonRedisSerializer); + template.afterPropertiesSet(); + + return template; + } +} +``` + +创建User对象,name和age + +测试存对象 + +```java +@SpringBootTest +public class RedisTest { + + @Autowired + private RedisTemplate redisTemplate; + + @Test + void test() { + User user = new User("致远",3); + redisTemplate.opsForValue().set("user",user); + System.out.println(redisTemplate.opsForValue().get("user")); + + } +} +``` + + + +### redis工具类 + +使用RedisTemplate需要频繁调用`.opForxxx`然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共API抽取出来封装成为一个工具类,然后直接使用工具类来间接操作Redis,不但效率高并且易用。 + +工具类参考博客: + +https://www.cnblogs.com/zeng1994/p/03303c805731afc9aa9c60dbbd32a323.html + +https://www.cnblogs.com/zhzhlong/p/11434284.html diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/10.Redis\345\255\246\344\271\240\347\254\224\350\256\260-\346\200\273\350\247\210.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/10.Redis\345\255\246\344\271\240\347\254\224\350\256\260-\346\200\273\350\247\210.md" new file mode 100644 index 00000000..eb125a72 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/10.Redis/01.Redis\345\255\246\344\271\240\347\254\224\350\256\260/10.Redis\345\255\246\344\271\240\347\254\224\350\256\260-\346\200\273\350\247\210.md" @@ -0,0 +1,832 @@ +--- +title: Redis学习笔记-总览 +permalink: /redis/study-note/10 +date: 2021-05-10 16:16:46 +--- + +## Redis.conf + +### 熟悉基本配置 + +> 位置 + +Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf + +```bash +config get * # 获取全部的配置 +``` + +我们一般情况下,会单独拷贝出来一份进行操作。来保证初始文件的安全。 + +正如在 `安装redis` 中的讲解中拷贝一份 + +> 容量单位不区分大小写,G和GB有区别 + +![image-20210408213939472](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408213939472.png) + +> include 组合多个配置 + +![image-20210408214037264](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408214037264.png) + +和Spring配置文件类似,可以通过includes包含,redis.conf 可以作为总文件,可以包含其他文件! + +> NETWORK 网络配置 + +```bash +bind 127.0.0.1 # 绑定的ip +protected-mode yes # 保护模式 +port 6379 # 默认端口 +``` + +> GENERAL 通用 + +```bash +daemonize yes # 默认情况下,Redis不作为守护进程运行。需要开启的话,改为 yes + +supervised no # 可通过upstart和systemd管理Redis守护进程 + +loglevel notice # 日志级别。可选项有: + # debug(记录大量日志信息,适用于开发、测试阶段) + # verbose(较多日志信息) + # notice(适量日志信息,使用于生产环境) + # warning(仅有部分重要、关键信息才会被记录) +logfile "" # 日志文件的位置,当指定为空字符串时,为标准输出 +databases 16 # 设置数据库的数目。默认的数据库是DB 0 +always-show-logo yes # 是否总是显示logo +``` + +> SNAPSHOPTING 快照,持久化规则 + +由于Redis是基于内存的数据库,需要将数据由内存持久化到文件中 + +持久化方式: + +- RDB +- AOF + +```bash +# 900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化) +save 900 1 +# 300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化) +save 300 10 +# 60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化) +save 60 10000 +``` + +RDB文件相关 + +```bash +stop-writes-on-bgsave-error yes # 持久化出现错误后,是否依然进行继续进行工作 + +rdbcompression yes # 使用压缩rdb文件 yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间 + +rdbchecksum yes # 是否校验rdb文件,更有利于文件的容错性,但是在保存rdb文件的时候,会有大概10%的性能损耗 + +dbfilename dump.rdb # dbfilenamerdb文件名称 + +dir ./ # dir 数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录 +``` + +> REPLICATION主从复制 + +![image-20210408214742862](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408214742862.png) + +后面详细说 + +> SECURITY安全 + +访问密码的查看,设置和取消 + +```bash +# 启动redis +# 连接客户端 + +# 获得和设置密码 +config get requirepass +config set requirepass "123456" + +#测试ping,发现需要验证 +127.0.0.1:6379> ping +NOAUTH Authentication required. +# 验证 +127.0.0.1:6379> auth 123456 +OK +127.0.0.1:6379> ping +PONG +``` + +> 客户端连接相关 + +```bash +maxclients 10000 最大客户端数量 +maxmemory 最大内存限制 +maxmemory-policy noeviction # 内存达到限制值的处理策略 +``` + +**maxmemory-policy 六种方式** + +**1、volatile-lru:**利用LRU算法移除设置过过期时间的key。 + +**2、allkeys-lru :** 用lru算法删除lkey + +**3、volatile-random:**随机删除即将过期key + +**4、allkeys-random:**随机删除 + +**5、volatile-ttl :** 删除即将过期的 + +**6、noeviction :** 不移除任何key,只是返回一个写错误。 + +redis 中的**默认**的过期策略是 **volatile-lru** 。 + +**设置方式** + +```bash +config set maxmemory-policy volatile-lru +``` + +> append only模式 + +(AOF相关部分) + +![在这里插入图片描述](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/20200513215037918.png) + +![在这里插入图片描述](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/20200513215047999.png) + +```bash +appendfsync everysec # appendfsync aof持久化策略的配置 + # no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。 + # always表示每次写入都执行fsync,以保证数据同步到磁盘。 + # everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。 +``` + +## Redis的持久化 + +Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能! + +**Rdb 保存的是 dump.rdb 文件** + + + +### RDB(Redis DataBase) + +> 什么是RDB + +在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快 照文件直接读到内存里。 + +Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。 这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。 + +> Fork + +Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量,环境变量,程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。 + + + +**配置位置及SNAPSHOTTING解析** + +![image-20210408224901985](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408224901985.png) + +这里的触发条件机制,我们可以修改测试一下: + +```bash +save 120 10 # 120秒内修改10次则触发RDB +``` + +RDB 是整合内存的压缩过的Snapshot,RDB 的数据结构,可以配置复合的快照触发条件。 + +如果想禁用RDB持久化的策略,只要不设置任何save指令,或者给save传入一个空字符串参数也可以。 + +若要修改完毕需要立马生效,可以手动使用 save 命令!立马生效 ! + +save不是创建一个新进程去进行持久化 + +> 其余命令解析 + +Stop-writes-on-bgsave-error:如果配置为no,表示你不在乎数据不一致或者有其他的手段发现和控制,默认为yes。 + +rbdcompression:对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用 LZF算法进行压缩,如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。 + +rdbchecksum:在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约 10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。默认为yes。 + +> 如何触发RDB快照 + +1、配置文件中默认的快照配置,建议多用一台机子作为备份,复制一份 dump.rdb + +2、命令save或者是bgsave + +- save 时只管保存,其他不管,全部阻塞 + +- bgsave,Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。可以通过lastsave + 命令获取最后一次成功执行快照的时间。 + +3、执行flushall命令,也会产生 dump.rdb 文件,但里面是空的,无意义 ! + +4、退出的时候也会产生 dump.rdb 文件! + +> 如何恢复 + +1、将备份文件(dump.rdb)移动到redis安装目录并启动服务即可 + +2、CONFIG GET dir 获取目录 + +```bash +127.0.0.1:6379> config get dir +dir +/usr/local/bin +``` + +> 优点和缺点 + +**优点:** + +1、适合大规模的数据恢复 + +2、对数据完整性和一致性要求不高 + +**缺点:** + +1、在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改 + +2、Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。 + +> 小结 + +![image-20210408225404338](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408225404338.png) + +### AOF(Append Only File) + +> 简介 + +以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件 的内容将写指令从前到后执行一次以完成数据的恢复工作 + +**Aof保存的是 appendonly.aof 文件** + +> 配置 + +![image-20210408225620719](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408225620719.png) + +- appendonly no + + 是否以append only模式作为持久化方式,默认使用的是rdb方式持久化,这 种方式在许多应用中已经足够用了 + +- appendfilename "appendonly.aof" + + appendfilename AOF 文件名称 + +- appendfsync everysec + + appendfsync aof持久化策略的配置 + + > no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。 + > always表示每次写入都执行fsync,以保证数据同步到磁盘。 + > everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。 + +- No-appendfsync-on-rewrite + + 重写时是否可以运用Appendfsync,用默认no即可,保证数据安全性 + +- Auto-aof-rewrite-min-size + + 设置重写的基准值 + +- Auto-aof-rewrite-percentage + + 设置重写的基准值 + +> AOF 启动/修复/恢复 + +**正常恢复:** + +启动:设置Yes,修改默认的appendonly no,改为yes +将有数据的aof文件复制一份保存到对应目录(config get dir) +恢复:重启redis然后重新加载 + +**异常恢复:** + +启动:设置Yes +故意破坏 appendonly.aof 文件! +修复:命令`redis-check-aof --fix appendonly.aof` 进行修复 +恢复:重启 redis 然后重新加载 + +> Rewrite重写 + +AOF 采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis 就会启动AOF 文件的内容压缩,只保留可以恢复数据的最小指令集,可以 使用命令 bgrewriteaof ! + +**重写原理:** + +AOF 文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再 rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧 的aof文件,这点和快照有点类似! + +**触发机制:** + +Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的已被且文件大 于64M的触发。 + +> 优点和缺点 + +**优点:** + +1、每修改同步:appendfsync always 同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好 + +2、每秒同步: appendfsync everysec 异步操作,每秒记录 ,如果一秒内宕机,有数据丢失 + +3、不同步: appendfsync no 从不同步 + +**缺点:** + +1、相同数据集的数据而言,aof 文件要远大于 rdb文件,恢复速度慢于 rdb。 + +2、Aof 运行效率要慢于 rdb,每秒同步策略效率较好,不同步效率和rdb相同。 + +> 小结 + +![image-20210408230339879](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210408230339879.png) + +### 总结 + +1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储 + +2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。 + +3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化 + +4、同时开启两种持久化方式 + +- 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF 文件保存的数据集要比RDB文件保存的数据集要完整。 +- RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有 AOF可能潜在的Bug,留着作为一个万一的手段。 + +5、性能建议 + +- 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够 了,只保留 save 900 1 这条规则。 +- 如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自 己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产 生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite 的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重 写可以改到适当的数值。 +- 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也 减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据, 启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。 + +## Redis 发布订阅 + +### 简介 + +Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。 + +Redis 客户端可以订阅任意数量的频道。 + +订阅/发布消息图: + +![image-20210409105838259](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409105838259.png) + +下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系: + +![image-20210409105859670](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409105859670.png) + +当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端: + +![image-20210409110314032](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409110314032.png) + +**命令** + +下表列出了 redis 发布订阅常用命令: + +| 序号 | 命令及描述 | +| :--- | :----------------------------------------------------------- | +| 1 | [PSUBSCRIBE pattern [pattern ...]]
订阅一个或多个符合给定模式的频道。 | +| 2 | [PUBSUB subcommand [argument [argument ...]]]
查看订阅与发布系统状态。 | +| 3 | [PUBLISH channel message]
将信息发送到指定的频道。 | +| 4 | [PUNSUBSCRIBE [pattern [pattern ...]]]
退订所有给定模式的频道。 | +| 5 | [SUBSCRIBE channel [channel ...]]
订阅给定的一个或多个频道的信息。 | +| 6 | [UNSUBSCRIBE [channel [channel ...]]]
指退订给定的频道。 | + +### 测试 + +以下实例演示了发布订阅是如何工作的。 + +我们先打开两个 redis-cli 客户端 + +在**第一个 redis-cli 客户端**,创建订阅频道名为 redisChat,输入SUBSCRIBE redisChat + +```bash +redis 127.0.0.1:6379> SUBSCRIBE redisChat +Reading messages... (press Ctrl-C to quit) +1) "subscribe" +2) "redisChat" +3) (integer) 1 +``` + +在**第二个客户端**,发布两次消息,订阅者就能接收 到消息。 + +```bash +redis 127.0.0.1:6379> PUBLISH redisChat "Hello,Redis" +(integer) 1 +redis 127.0.0.1:6379> PUBLISH redisChat "Hello,java" +(integer) 1 +``` + +订阅者的客户端会显示如下消息 + +> 1) "message" +> 2) "redisChat" +> 3) "Hello,Redis" +> 1) "message" +> 2) "redisChat" +> 3) "Hello,java" + +### 原理 + +Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,来加深对 Redis 的理解。 + +Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。 + +通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 channel ,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。 + +通过 PUBLISH 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。 + +Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个 key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。 + +使用场景: + +Redis的Pub/Sub系统可以构建实时的消息系统,比如很多用Pub/Sub构建的实时聊天系统的例子。 + +## Redis主从复制 + +### 概念 + +主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点 (master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。 Master以写为主,Slave 以读为主。 + +默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。 + +主从复制的作用主要包括: + +1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。 + +2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。 + +3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。 + +4、高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis高可用的基础。 + + + +一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下: + +1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较 大; + +2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。 + +电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。 + +对于这种场景,我们可以使如下这种架构: + +![image-20210409112108209](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409112108209.png) + +### 环境配置 + +> 基本配置 + + + +查看当前库的信息:`info replication` + +```bash +127.0.0.1:6379> info replication +# Replication +role:master # 角色 +connected_slaves:0 # 从机数量 +master_failover_state:no-failover +master_replid:1a6933acf7ec9711bfa0a1848976676557e1e6a0 +master_replid2:0000000000000000000000000000000000000000 +master_repl_offset:0 +second_repl_offset:-1 +repl_backlog_active:0 +repl_backlog_size:1048576 +repl_backlog_first_byte_offset:0 +repl_backlog_histlen:0 +127.0.0.1:6379> +``` + +因为没有多个服务器,就以本地开启3个端口,模拟3个服务 + +既然需要启动多个服务,就需要多个配置文件。每个配置文件对应修改以下信息: + +- 端口号(port) +- pid文件名(pidfile) +- 日志文件名(logfile) +- rdb文件名(dbfilename) + +1、拷贝多个redis.conf 文件 + +端口分别是6379、6380、6381 + +```bash +[root@localhost ~]# cd /usr/local/bin/myconfig +[root@localhost myconfig]# ls +dump.rdb redis.conf +[root@localhost myconfig]# cp redis.conf redis79.conf +[root@localhost myconfig]# cp redis.conf redis80.conf +[root@localhost myconfig]# cp redis.conf redis81.conf +[root@localhost myconfig]# ls +dump.rdb redis79.conf redis80.conf redis81.conf redis.conf +``` + +分别修改配置上面四点对应的配置,举例: + +![image-20210409115138265](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409115138265.png) + +![image-20210409115114373](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409115114373.png) + +![image-20210409115241761](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409115241761.png) + + + +配置好分别启动3个不同端口服务 + +- redis-server myconfig/redis79.conf + +- redis-server myconfig/redis80.conf + +- redis-server myconfig/redis81.conf + +redis-server myconfig/redis79.conf + +![在这里插入图片描述](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/20200513215610163.png) + +### 一主二从 + +1、之后我们再分别开启redis连接,redis-cli -p 6379,redis-cli -p 6380,redis-cli -p 6381 + +通过指令 + +```bash +127.0.0.1:6379> info replication +``` + +可以发现,默认情况下,开启的每个redis服务器都是主节点 + +![image-20210409134833129](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409134833129.png) + + + +2、配置为一个Master 两个Slave(即一主二从) + +6379为主,6380,6381为从 + +slaveof 127.0.0.1 6379 + +![image-20210409134929416](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409134929416.png) + +3、在主机设置值,在从机都可以取到!从机不能写值! + +![image-20210409135325865](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409135325865.png) + +我们这里是使用命令搭建,是“暂时的”,也可去配置里进行修改,这样话则是“永久的” + +![image-20210409135633320](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409135633320.png) + +> 使用规则 + +当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。 + +当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。 + +有两种方式可以产生新的主机:看下文“谋权篡位” + +> 层层链路 + +上一个Slave 可以是下一个slave 和 Master,Slave 同样可以接收其他 slaves 的连接和同步请求,那么 该 slave 作为了链条中下一个的master,可以有效减轻 master 的写压力! + +![image-20210409135939928](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409135939928.png) + +![image-20210409135955890](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409135955890.png) + + + +> 谋权篡位 + + + +有两种方式可以产生新的主机: + +- 从机手动执行命令`slaveof no one`,这样执行以后从机会独立出来成为一个主机 +- 使用哨兵模式(自动选举) + +> 复制原理 + +Slave 启动成功连接到 master 后会发送一个sync命令 + +Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行 完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。 + +全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。 + +增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步 + +但是只要是重新连接master,一次完全同步(全量复制)将被自动执行 + +### 哨兵模式 + +更多信息参考博客:https://www.jianshu.com/p/06ab9daf921d + +> 概述 + +主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工 干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑 哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。 + +谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。 + +哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独 立运行。其原理是**哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。** + +![image-20210409150628118](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409150628118.png) + +这里的哨兵有两个作用 + +- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。 +- 当哨兵监测到master宕机,会自动将slave切换成master,然后通过**发布订阅模式**通知其他的从服务器,修改配置文件,让它们切换主机。 + +然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。 各个哨兵之间还会进行监控,这样就形成了多哨兵模式。 + +![image-20210409150717930](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/image-20210409150717930.png) + +假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认 为主服务器不可用,这个现象成为**主观下线**。当后面的哨兵也检测到主服务器不可用,并且数量达到一 定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。 切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为 **客观下线**。 + +> 配置测试 + +1、调整结构,6379带着80、81 + +2、自定义的 /myconfig 目录下新建 sentinel.conf 文件,名字千万不要错 + +3、配置哨兵,填写内容 + +- `sentinel monitor 被监控主机名字 127.0.0.1 6379 1` + + 例如:sentinel monitor mymaster 127.0.0.1 6379 1, + + 上面最后一个数字1,表示主机挂掉后slave投票看让谁接替成为主机,得票数多少后成为主机 + +4、启动哨兵 + +- Redis-sentinel myconfig/sentinel.conf + + 上述目录依照各自的实际情况配置,可能目录不同 + +成功启动哨兵模式 + +![在这里插入图片描述](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/20200513215752444.png) + +此时哨兵监视着我们的主机6379,当我们断开主机后: + +![在这里插入图片描述](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/20200513215806972.png) + +> 哨兵模式的优缺点 + +**优点** + +1. 哨兵集群,基于主从复制模式,所有主从复制的优点,它都有 +2. 主从可以切换,故障可以转移,系统的可用性更好 +3. 哨兵模式是主从模式的升级,手动到自动,更加健壮 + +**缺点:** + +1. Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦 +2. 实现哨兵模式的配置其实是很麻烦的,里面有很多配置项 + + + +> 哨兵模式的全部配置 + +完整的哨兵模式配置文件 sentinel.conf + +```bash +# Example sentinel.conf + +# 哨兵sentinel实例运行的端口 默认26379 +port 26379 + +# 哨兵sentinel的工作目录 +dir /tmp + +# 哨兵sentinel监控的redis主节点的 ip port +# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。 +# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了 +# sentinel monitor +sentinel monitor mymaster 127.0.0.1 6379 1 + +# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码 +# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码 +# sentinel auth-pass +sentinel auth-pass mymaster MySUPER--secret-0123passw0rd + + +# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒 +# sentinel down-after-milliseconds +sentinel down-after-milliseconds mymaster 30000 + +# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步, +这个数字越小,完成failover所需的时间就越长, +但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。 +可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。 +# sentinel parallel-syncs +sentinel parallel-syncs mymaster 1 + + + +# 故障转移的超时时间 failover-timeout 可以用在以下这些方面: +#1. 同一个sentinel对同一个master两次failover之间的间隔时间。 +#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。 +#3.当想要取消一个正在进行的failover所需要的时间。 +#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了 +# 默认三分钟 +# sentinel failover-timeout +sentinel failover-timeout mymaster 180000 + +# SCRIPTS EXECUTION + +#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。 +#对于脚本的运行结果有以下规则: +#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10 +#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。 +#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。 +#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。 + +#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本, +#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数, +#一个是事件的类型, +#一个是事件的描述。 +#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。 +#通知脚本 +# sentinel notification-script + sentinel notification-script mymaster /var/redis/notify.sh + +# 客户端重新配置主节点参数脚本 +# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。 +# 以下参数将会在调用脚本时传给脚本: +# +# 目前总是“failover”, +# 是“leader”或者“observer”中的一个。 +# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的 +# 这个脚本应该是通用的,能被多次调用,不是针对性的。 +# sentinel client-reconfig-script +sentinel client-reconfig-script mymaster /var/redis/reconfig.sh +``` + +## 缓存穿透与雪崩 + +### 缓存穿透(查不到) + +> 概念 + +在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。 + +> 解决方案 + +**布隆过滤器** + +对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。 + +![在这里插入图片描述](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/20200513215824722.jpg) + +**缓存空对象** + +一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。 + +![在这里插入图片描述](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/20200513215836317.jpg) + + 这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间 + +即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。 + +### 缓存击穿(量太大,缓存过期) + +> 概念 + + 相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。 + + 比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。 + +> 解决方案 + +1. **设置热点数据永不过期** + + 这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。 + +2. **加互斥锁(分布式锁)** + + 在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。 + +### 缓存雪崩 + +> 概念 + +大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。 + +产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。 + +![在这里插入图片描述](https://cdn.jsdelivr.net/gh/oddfar/static/img/Redis.assets/20200513215850428.jpeg) + +其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然 形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就 是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知 的,很有可能瞬间就把数据库压垮。 + +> 解决方案 + +- redis高可用 + + 这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群 + +- 限流降级 + + 这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。 + +- 数据预热 + + 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。 diff --git "a/docs/03.\346\225\260\346\215\256\345\272\223/15.ElasticSearch/02.\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/docs/03.\346\225\260\346\215\256\345\272\223/15.ElasticSearch/02.\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 00000000..040fde99 --- /dev/null +++ "b/docs/03.\346\225\260\346\215\256\345\272\223/15.ElasticSearch/02.\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,14 @@ +--- +title: ElasticSearch +permalink: /elasticsearch/note/2/ +date: 2021-05-18 16:25:00 +--- + + + + + +# 总览 + +ElasticSearch + diff --git "a/docs/04.\346\241\206\346\236\266/01.mybatis/01.\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/docs/04.\346\241\206\346\236\266/01.mybatis/01.\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 00000000..34e57a8c --- /dev/null +++ "b/docs/04.\346\241\206\346\236\266/01.mybatis/01.\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,2087 @@ +--- +title: mybatis学习笔记 +permalink: /mybatis/study-note +date: 2021-05-09 09:53:35 + +--- + +# Mybatis + +

狂神mybatis视频教程:https://www.bilibili.com/video/BV1NE411Q7Nx

+ + +## 1、简介 + +### 1.1、什么是Mybatis + +![1569633932712](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis.assets/1569633932712.png) + +- MyBatis 是一款优秀的**持久层框架** +- 它支持定制化 SQL、存储过程以及高级映射。 +- MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 +- MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 +- MyBatis 本是[apache](https://baike.baidu.com/item/apache/6265)的一个开源项目[iBatis](https://baike.baidu.com/item/iBatis), 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。 +- 2013年11月迁移到Github。 + + + +如何获得Mybatis? + +- maven仓库: + + ```xml + + + org.mybatis + mybatis + 3.5.2 + + ``` + +- Github : https://github.com/mybatis/mybatis-3/releases + +- 中文文档:https://mybatis.org/mybatis-3/zh/index.html + + + +### 1.2、持久化 + +数据持久化 + +- 持久化就是将程序的数据在持久状态和瞬时状态转化的过程 +- 内存:**断电即失** +- 数据库(Jdbc),io文件持久化。 +- 生活:冷藏. 罐头。 + +**为什么需要需要持久化?** + +- 有一些对象,不能让他丢掉。 + +- 内存太贵了 + + + +### 1.3、持久层 + +Dao层,Service层,Controller层…. + +- 完成持久化工作的代码块 +- 层界限十分明显。 + + + +### 1.4 为什么需要Mybatis? + +- 帮助程序猿将数据存入到数据库中。 +- 方便 +- 传统的JDBC代码太复杂了。简化。框架。自动化。 +- 不用Mybatis也可以。更容易上手。 **技术没有高低之分** +- 优点: + - 简单易学 + - 灵活 + - sql和代码的分离,提高了可维护性。 + - 提供映射标签,支持对象与数据库的orm字段关系映射 + - 提供对象关系映射标签,支持对象关系组建维护 + - 提供xml标签,支持编写动态sql。 + +**最重要的一点:使用的人多!** + + + +## 2、第一个Mybatis程序 + +创建模块`mybatis-01` 代码文件如下: + +![image-20210412205636100](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis.assets/image-20210412205636100.png) + +思路:搭建环境-->导入Mybatis-->编写代码-->测试! + +### 2.1、搭建环境 + +搭建数据库 + +```java +CREATE DATABASE `mybatis`; + +USE `mybatis`; + +CREATE TABLE `user`( + `id` INT(20) NOT NULL PRIMARY KEY, + `name` VARCHAR(30) DEFAULT NULL, + `pwd` VARCHAR(30) DEFAULT NULL +)ENGINE=INNODB DEFAULT CHARSET=utf8; + +INSERT INTO `user`(`id`,`name`,`pwd`) VALUES +(1,'狂神','123456'), +(2,'张三','123456'), +(3,'李四','123890') +``` + +新建项目 + +1. 新建一个普通的maven项目 + +2. 删除src目录 + +3. 导入maven依赖及静态文件导出配置 + + ```xml + + + + + mysql + mysql-connector-java + 5.1.47 + + + + + org.mybatis + mybatis + 3.5.2 + + + + junit + junit + 4.12 + + + + + + + + src/main/resources + + **/*.properties + **/*.xml + + true + + + src/main/java + + **/*.properties + **/*.xml + + true + + + + ``` + +注:若mysql驱动版本为8.x版本,需要配置mysql时区,还需更改驱动为`com.mysql.cj.jdbc.Driver` + +### 2.2、创建一个模块 + +- 编写mybatis的核心配置文件 + + 在`resources`下添加`mybatis-config.xml` + + ```xml + + + + + + + + + + + + + + + + + + + ``` + + 如果要添加中文注释,把第一行的`encoding="UTF-8" `改成`encoding="utf8"` + +- 编写mybatis工具类 + + ```java + public class MybatisUtils { + private static SqlSessionFactory sqlSessionFactory; + + static { + try { + //使用Mybatis第一步:获取sqlSessionFactory对象 + String resource = "mybatis-config.xml"; + InputStream inputStream = Resources.getResourceAsStream(resource); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + /** + * 既然有了 SqlSessionFactory,顾名思义,我们就可以从中获得 SqlSession 的实例了。 + * SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。 + * @return SqlSession + */ + public static SqlSession getSqlSession() { + return sqlSessionFactory.openSession(); + } + } + ``` + +### 2.3、编写代码 + +- 实体类 + + ```java + //实体类 + public class User { + private int id; + private String name; + private String pwd; + + public User() { + } + + public User(int id, String name, String pwd) { + this.id = id; + this.name = name; + this.pwd = pwd; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPwd() { + return pwd; + } + + public void setPwd(String pwd) { + this.pwd = pwd; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", name='" + name + '\'' + + ", pwd='" + pwd + '\'' + + '}'; + } + } + + ``` + +- Dao接口 + + ```java + public interface UserDao { + /** + * 执行sql,获取list合集 + * @return + */ + List getUserList(); + } + ``` + +- 接口实现类由原来的UserDaoImpl转变为一个 Mapper配置文件 + + UserMapper.xml + + ```xml + + + + + + + + + + ``` + + + +### 2.4、测试 + +核心配置文件中注册 mappers + +- junit测试 + + ```java + @Test + public void test(){ + //第一步:获得SqlSession对象 + SqlSession sqlSession = MybatisUtils.getSqlSession(); + + + //方式一:getMapper + UserDao userDao = sqlSession.getMapper(UserDao.class); + List userList = userDao.getUserList(); + + for (User user : userList) { + System.out.println(user); + } + + + + //关闭SqlSession + sqlSession.close(); + } + + ``` + + + +你们可以能会遇到的问题: + +1. 配置文件没有注册 +2. 绑定接口错误。 +3. 方法名不对 +4. 返回类型不对 +5. Maven导出资源问题 + + + +## 3、CRUD + + + +### 1、namespace + +namespace中的包名要和 Dao/mapper 接口的包名一致! + +### 2、select + +选择,查询语句; + +- id : 就是对应的namespace中的方法名; + +- resultType:Sql语句执行的返回值! + +- parameterType : 参数类型! + + + +1. 编写接口 + + ```java + //根据ID查询用户 + User getUserById(int id); + ``` + +2. 编写对应的mapper中的sql语句 + + ```java + + ``` + +3. 测试 + + ```java + @Test + public void getUserById() { + SqlSession sqlSession = MybatisUtils.getSqlSession(); + + UserMapper mapper = sqlSession.getMapper(UserMapper.class); + + User user = mapper.getUserById(1); + System.out.println(user); + + sqlSession.close(); + } + ``` + +### 3、Insert + +```xml + + + insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd}); + +``` + +```java +@Test +public void addUser() { + SqlSession sqlSession = MybatisUtils.getSqlSession(); + + UserMapper mapper = sqlSession.getMapper(UserMapper.class); + int res = mapper.addUser(new User(4, "zhiyuan", "212313")); + if (res > 0) { + //提交事务 + sqlSession.commit(); + System.out.println("添加成功"); + } + sqlSession.close(); +} +``` + +### 4、update + +```xml + + update mybatis.user set name=#{name},pwd=#{pwd} where id = #{id} ; + +``` + +### 5、Delete + +```xml + + delete from mybatis.user where id = #{id}; + +``` + + + +注意点: + +- 增删改需要提交事务! + + sqlSession.commit(); + + + +### 6、分析错误 + +- 标签不要匹配错 +- resource 绑定mapper,需要使用路径! +- 程序配置文件必须符合规范! +- NullPointerException,没有注册到资源! +- 输出的xml文件中存在中文乱码问题! +- maven资源没有导出问题! + + + +### 7、万能Map + +假设,我们的实体类,或者数据库中的表,字段或者参数过多,我们应当考虑使用Map! + +```java +//万能的Map +int addUser2(Map map); +``` + +```xml + + + insert into mybatis.user (id,name,pwd) values (#{userid},#{username},#{passWord}); + +``` + +```java +/** + * 用map的形式插入数据 + */ +@Test +public void addUser2() { + SqlSession sqlSession = MybatisUtils.getSqlSession(); + + UserMapper mapper = sqlSession.getMapper(UserMapper.class); + Map map = new HashMap(); + map.put("userid",5); + map.put("passWord","2222333"); + map.put("username","map"); + + int res = mapper.addUser2(map); + if (res > 0) { + //提交事务 + sqlSession.commit(); + System.out.println("添加成功"); + } + sqlSession.close(); +} +``` + + + +Map传递参数,直接在sql中取出key即可! 【parameterType="map"】 + +对象传递参数,直接在sql中取对象的属性即可!【parameterType="Object"】 + +只有一个基本类型参数的情况下,可以直接在sql中取到! + +多个参数用Map,**或者注解!** + +### 8、思考题 + +模糊查询怎么写? + +有两种方式 + +- Java代码执行的时候,传递通配符 % % + +```java +List userList = mapper.getUserLike("%李%"); +``` + +这是我们指定查询`"%李%"`,如果放个参数获取,这种方式则不能避免sql注入问题 + +- 在sql拼接中使用通配符! + +```java +select * from mybatis.user where name like "%"#{value}"%" +``` + +推荐使用这种方法 + + + +## 4、配置解析 + +创建模块`mybatis-02` + +### 1、核心配置文件 + +- mybatis-config.xml + +- MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 + + ```xml + configuration(配置) + properties(属性) + settings(设置) + typeAliases(类型别名) + typeHandlers(类型处理器) + objectFactory(对象工厂) + plugins(插件) + environments(环境配置) + environment(环境变量) + transactionManager(事务管理器) + dataSource(数据源) + databaseIdProvider(数据库厂商标识) + mappers(映射器) + ``` + +### 2、环境配置(environments) + +MyBatis 可以配置成适应多种环境 + +**不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。** + +如果要想再连接其它数据库,则需要重新再写个MybatisUtils工具类 + +学会使用配置多套运行环境! + +Mybatis默认的事务管理器就是 JDBC , 连接池 : POOLED + +### 3、属性(properties) + +我们可以通过properties属性来实现引用配置文件 + +这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来传递。 + +![1569656528134](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569656528134.png) + +编写一个配置文件db.properties + +```properties +driver=com.mysql.jdbc.Driver +url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8 +username=root +password=123456 +``` + +在核心配置文件中映入 + +```xml + + + + + +``` + +- 可以直接引入外部文件 +- 可以在其中增加一些属性配置 +- 如果两个文件有同一个字段,优先使用外部配置文件的! + + + +### 4、类型别名(typeAliases) + +- 类型别名是为 Java 类型设置一个短的名字。‘ +- 存在的意义仅在于用来减少类完全限定名的冗余。 + +```xml + + + + +``` + +也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如: + +扫描实体类的包,它的默认别名就为这个类的类名,首字母小写! + +```xml + + + + +``` + + + +在实体类比较少的时候,使用第一种方式。 + +如果实体类十分多,建议使用第二种。 + +第一种可以自定义别名,第二种则不行(别名为文件名),如果非要改,需要在实体上增加注解 + +```java +@Alias("user") +public class User {} +``` + +### 5、设置 + +这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 + +![1569657659080](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569657659080.png) + +![1569657672791](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569657672791.png) + +### 6、其他配置 + +- [typeHandlers(类型处理器)](https://mybatis.org/mybatis-3/zh/configuration.html#typeHandlers) +- [objectFactory(对象工厂)](https://mybatis.org/mybatis-3/zh/configuration.html#objectFactory) +- plugins插件 + - mybatis-generator-core + - mybatis-plus + - 通用mapper + +### 7、映射器(mappers) + +MapperRegistry:注册绑定我们的Mapper文件; + +方式一: 【推荐使用】 + +```xml + + + + +``` + +方式二:使用class文件绑定注册 + +```xml + + + + +``` + +注意点: + +- 接口和他的Mapper配置文件必须同名! +- 接口和他的Mapper配置文件必须在同一个包下! + + + +方式三:使用扫描包进行注入绑定 + +```xml + + + + +``` + +注意点: + +- 接口和他的Mapper配置文件必须同名! +- 接口和他的Mapper配置文件必须在同一个包下! + + + +练习时间: + +- 将数据库配置文件外部引入 +- 实体类别名 +- 保证UserMapper 接口 和 UserMapper .xml 改为一致!并且放在同一个包下! + + + +### 8、生命周期和作用域 + +![1569660357745](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569660357745.png) + +生命周期,和作用域,是至关重要的,因为错误的使用会导致非常严重的**并发问题**。 + +**SqlSessionFactoryBuilder:** + +- 一旦创建了 SqlSessionFactory,就不再需要它了 +- 局部变量 + +**SqlSessionFactory:** + +- 说白了就是可以想象为 :数据库连接池 +- SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,**没有任何理由丢弃它或重新创建另一个实例。** +- 因此 SqlSessionFactory 的最佳作用域是应用作用域。 +- 最简单的就是使用**单例模式**或者静态单例模式。 + +**SqlSession** + +- 连接到连接池的一个请求! +- SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 +- 用完之后需要赶紧关闭,否则资源被占用! + +![1569660737088](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569660737088.png) + +这里面的每一个Mapper,就代表一个具体的业务! + + + +## 5、属性名和字段名配置 + +在UserMapper.xml文件中 + +如果我们的sql语句中属性名和字段名不一样,则会查不到数据 + +新建`mybatis-03`模块 + +### 1、 问题 + +数据库中的字段 + +![1569660831076](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569660831076.png) + +新建一个项目,拷贝之前的,测试实体类字段不一致的情况 + +```java +public class User { + private int id; + private String name; + private String password; +} +``` + +编写sql,测试出现问题 + +![1569661145806](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569661145806.png) + + + +解决方法有两种,一种起别名,一种resultMap(结果集映射) + +- 起别名 + + ```xml + + ``` + + + +### 2、resultMap + +结果集映射 + +``` +id name pwd +id name password +``` + +```xml + + + + + + + + + +``` + + + +- `resultMap` 元素是 MyBatis 中最重要最强大的元素 +- ResultMap 的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了。 +- `ResultMap` 最优秀的地方在于,虽然你已经对它相当了解了,但是根本就不需要显式地用到他们。 +- 如果世界总是这么简单就好了。 + + + +## 6、日志 + +新建模块`mybatis-04` + + + +### 6.1、日志工厂 + +如果一个数据库操作,出现了异常,我们需要排错。日志就是最好的助手! + +曾经:sout 、debug + +现在:日志工厂! + +![1569892155104](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569892155104.png) + +- SLF4J + +- LOG4J 【掌握】 +- LOG4J2 +- JDK_LOGGING +- COMMONS_LOGGING +- STDOUT_LOGGING 【掌握】 +- NO_LOGGING + + + +在Mybatis中具体使用那个一日志实现,在设置中设定! + +**STDOUT_LOGGING标准日志输出** + +在mybatis核心配置文件中,配置我们的日志! + +```xml + + + +``` + +![1569892595060](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569892595060.png) + + + +### 6.2、Log4j + +什么是Log4j? + +- Log4j是[Apache](https://baike.baidu.com/item/Apache/8512995)的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是[控制台](https://baike.baidu.com/item/控制台/2438626)、文件、[GUI](https://baike.baidu.com/item/GUI)组件 +- 我们也可以控制每一条日志的输出格式; +- 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。 +- 通过一个[配置文件](https://baike.baidu.com/item/配置文件/286550)来灵活地进行配置,而不需要修改应用的代码。 + + + +1. 先导入log4j的包 + + ```xml + + + log4j + log4j + 1.2.17 + + ``` + +2. log4j.properties + + ```properties + #将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码 + log4j.rootLogger=DEBUG,console,file + + #控制台输出的相关设置 + log4j.appender.console = org.apache.log4j.ConsoleAppender + log4j.appender.console.Target = System.out + log4j.appender.console.Threshold=DEBUG + log4j.appender.console.layout = org.apache.log4j.PatternLayout + log4j.appender.console.layout.ConversionPattern=[%c]-%m%n + + #文件输出的相关设置 + log4j.appender.file = org.apache.log4j.RollingFileAppender + log4j.appender.file.File=./log/kuang.log + log4j.appender.file.MaxFileSize=10mb + log4j.appender.file.Threshold=DEBUG + log4j.appender.file.layout=org.apache.log4j.PatternLayout + log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n + + #日志输出级别 + log4j.logger.org.mybatis=DEBUG + log4j.logger.java.sql=DEBUG + log4j.logger.java.sql.Statement=DEBUG + log4j.logger.java.sql.ResultSet=DEBUG + log4j.logger.java.sql.PreparedStatement=DEBUG + ``` + +3. 配置log4j为日志的实现 + + ```xml + + + + ``` + +4. Log4j的使用!,直接测试运行刚才的查询 + + ![1569893505842](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569893505842.png) + + + +**简单使用** + +1. 在要使用Log4j 的类中,导入包 import org.apache.log4j.Logger; + +2. 日志对象,参数为当前类的class + + ```java + static Logger logger = Logger.getLogger(UserDaoTest.class); + ``` + +3. 日志级别 + + ```java + logger.info("info:进入了testLog4j"); + logger.debug("debug:进入了testLog4j"); + logger.error("error:进入了testLog4j"); + ``` + + + +## 7、分页 + + + +**思考:为什么要分页?** + +- 减少数据的处理量 + + + +### 7.1、使用Limit分页 + +```sql +语法:SELECT * from user limit startIndex,pageSize; +SELECT * from user limit 3; #[0,n] +``` + + + +使用Mybatis实现分页,核心SQL + +1. 接口 + + ```java + //分页 + List getUserByLimit(Map map); + ``` + +2. Mapper.xml + + ```xml + + + ``` + +3. 测试 + + ```java + @Test + public void getUserByLimit(){ + SqlSession sqlSession = MybatisUtils.getSqlSession(); + UserMapper mapper = sqlSession.getMapper(UserMapper.class); + + HashMap map = new HashMap(); + map.put("startIndex",1); + map.put("pageSize",2); + + List userList = mapper.getUserByLimit(map); + for (User user : userList) { + System.out.println(user); + } + + sqlSession.close(); + } + } + ``` + + + +### 7.2、RowBounds分页 + +不再使用SQL实现分页 + +1. 接口 + + ```java + //分页2 + List getUserByRowBounds(); + ``` + +2. mapper.xml + + ```xml + + + ``` + +3. 测试 + + ```java + @Test + public void getUserByRowBounds(){ + SqlSession sqlSession = MybatisUtils.getSqlSession(); + + //RowBounds实现 + RowBounds rowBounds = new RowBounds(1, 2); + + //通过Java代码层面实现分页 + List userList = sqlSession.selectList("com.kuang.dao.UserMapper.getUserByRowBounds",null,rowBounds); + + for (User user : userList) { + System.out.println(user); + } + + sqlSession.close(); + } + ``` + + + +### 7.3、分页插件 + +![1569896603103](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569896603103.png) + +了解即可,万一 以后公司的架构师,说要使用,你需要知道它是什么东西! + + + +## 8、使用注解开发 + +对应模块`mybaits-05` + + + +### 8.1、面向接口编程 + +大家之前都学过面向对象编程,也学习过接口,但在真正的开发中,很多时候我们会选择面向接口编程 + +**根本原因 : 解耦 , 可拓展 , 提高复用 , 分层开发中 , 上层不用管具体的实现 , 大家都遵守共同的标准 , 使得开发变得容易 , 规范性更好** + +在一个面向对象的系统中,系统的各种功能是由许许多多的不同对象协作完成的。在这种情况下,各个对象内部是如何实现自己的,对系统设计人员来讲就不那么重要了; + +而各个对象之间的协作关系则成为系统设计的关键。小到不同类之间的通信,大到各模块之间的交互,在系统设计之初都是要着重考虑的,这也是系统设计的主要工作内容。面向接口编程就是指按照这种思想来编程。 + + + +**关于接口的理解** + +- 接口从更深层次的理解,应是定义(规范,约束)与实现(名实分离的原则)的分离。 +- 接口的本身反映了系统设计人员对系统的抽象理解。 +- 接口应有两类: + - 第一类是对一个个体的抽象,它可对应为一个抽象体(abstract class); + - 第二类是对一个个体某一方面的抽象,即形成一个抽象面(interface); +- 一个体有可能有多个抽象面。抽象体与抽象面是有区别的。 + + + +**三个面向区别** + +- 面向对象是指,我们考虑问题时,以对象为单位,考虑它的属性及方法 . +- 面向过程是指,我们考虑问题时,以一个具体的流程(事务过程)为单位,考虑它的实现 . +- 接口设计与非接口设计是针对复用技术而言的,与面向对象(过程)不是一个问题.更多的体现就是对系统整体的架构 + +### 8.2、使用注解开发 + +1. 注解在接口上实现 + + ```java + @Select("select * from user") + List getUsers(); + ``` + +2. 需要再核心配置文件中绑定接口! + + ```xml + + + + + ``` + +3. 测试 + + + +本质:反射机制实现 + +底层:动态代理! + + ![1569898830704](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569898830704.png) + + + +**Mybatis详细的执行流程!** + +![1569898830704](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//Temp.png) + + + +### 8.3、CRUD + +我们可以在工具类创建的时候实现自动提交事务! + +```java +public static SqlSession getSqlSession(){ + return sqlSessionFactory.openSession(true); +} +``` + + + +编写接口,增加注解 + +```java +public interface UserMapper { + + @Select("select * from user") + List getUsers(); + + // 方法存在多个参数,所有的参数前面必须加上 @Param("id")注解 + @Select("select * from user where id = #{id}") + User getUserByID(@Param("id") int id); + + + @Insert("insert into user(id,name,pwd) values (#{id},#{name},#{password})") + int addUser(User user); + + + @Update("update user set name=#{name},pwd=#{password} where id = #{id}") + int updateUser(User user); + + + @Delete("delete from user where id = #{uid}") + int deleteUser(@Param("uid") int id); +} +``` + + + +测试类 + +【注意:我们必须要讲接口注册绑定到我们的核心配置文件中!】 + + + +**关于@Param() 注解** + +- 基本类型的参数或者String类型,需要加上 +- 引用类型不需要加 +- 如果只有一个基本类型的话,可以忽略,但是建议大家都加上! +- 我们在SQL中引用的就是我们这里的 @Param() 中设定的属性名! + + + +**#{} ${} 区别** + + + +## 9、Lombok + +对应模块`mybatis-06` + +```java +Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java. +Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more. +``` + +- java library +- plugs +- build tools +- with one annotation your class + + + +使用步骤: + +1. 在IDEA中安装Lombok插件! + +2. 在项目中导入lombok的jar包 + + ```xml + + org.projectlombok + lombok + 1.18.10 + + ``` + +3. 在实体类上加注解即可! + + ```java + @Data + @AllArgsConstructor + @NoArgsConstructor + ``` + + + +```java +@Getter and @Setter +@FieldNameConstants +@ToString +@EqualsAndHashCode +@AllArgsConstructor, @RequiredArgsConstructor and @NoArgsConstructor +@Log, @Log4j, @Log4j2, @Slf4j, @XSlf4j, @CommonsLog, @JBossLog, @Flogger +@Data +@Builder +@Singular +@Delegate +@Value +@Accessors +@Wither +@SneakyThrows +``` + +说明: + +``` +@Data:无参构造,get、set、tostring、hashcode,equals +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode +@ToString +@Getter +``` + + + +## 10、多对一处理 + +对应模块`mybatis-06` + +多对一: + +![1569909163944](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569909163944.png) + +- 多个学生,对应一个老师 +- 对于学生这边而言, **关联** .. 多个学生,关联一个老师 【多对一】 +- 对于老师而言, **集合** , 一个老师,有很多学生 【一对多】 + +![1569909422471](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569909422471.png) + +pojo: + +```java +@Data +public class Student { + private int id; + private String name; + private Teacher teacher; +} +``` + +```java +@Data +public class Teacher { + private int id; + private String name; +} +``` + +SQL: + +```sql +CREATE TABLE `teacher` ( + `id` INT(10) NOT NULL, + `name` VARCHAR(30) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=INNODB DEFAULT CHARSET=utf8 + +INSERT INTO teacher(`id`, `name`) VALUES (1, '秦老师'); + +CREATE TABLE `student` ( + `id` INT(10) NOT NULL, + `name` VARCHAR(30) DEFAULT NULL, + `tid` INT(10) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fktid` (`tid`), + CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`) +) ENGINE=INNODB DEFAULT CHARSET=utf8 + + +INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('1', '小明', '1'); +INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('2', '小红', '1'); +INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('3', '小张', '1'); +INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('4', '小李', '1'); +INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('5', '小王', '1'); + +``` + + + +### 测试环境搭建 + +1. 导入lombok +2. 新建实体类 Teacher,Student +3. 建立Mapper接口 +4. 建立Mapper.XML文件 +5. 在核心配置文件中绑定注册我们的Mapper接口或者文件!【方式很多,随心选】 +6. 测试查询是否能够成功! + + + +### 按照查询嵌套处理 + +```xml + + + + + + + + + + + + + +``` + + + +### 按照结果嵌套处理 + +```xml + + + + + + + + + + +``` + + + +回顾Mysql 多对一查询方式: + +- 子查询 +- 联表查询 + + + +## 11、一对多处理 + +对应模块`mybatis-07` + +比如:一个老师拥有多个学生! + +对于老师而言,就是一对多的关系! + +### 环境搭建 + +环境搭建,和刚才一样 + +**实体类** + +```java +@Data +public class Student { + private int id; + private String name; + private int tid; +} +``` + +```java +@Data +public class Teacher { + private int id; + private String name; + //一个老师拥有多个学生 + private List students; +} +``` + + + + + +### 按照查询嵌套处理 + +```xml + + + + + + + +``` + +### 按照结果嵌套处理 + + + +```xml + + + + + + + + + + + + + +``` + + + +### 小结 + +1. 关联 - association 【多对一】 +2. 集合 - collection 【一对多】 +3. javaType & ofType + 1. JavaType 用来指定实体类中属性的类型 + 2. ofType 用来指定映射到List或者集合中的 pojo类型,泛型中的约束类型! + +注意点: + +- 保证SQL的可读性,尽量保证通俗易懂 +- 注意一对多和多对一中,属性名和字段的问题! +- 如果问题不好排查错误,可以使用日志 , 建议使用 Log4j + + + +**慢SQL 1s 1000s** + +面试高频 + +- Mysql引擎 +- InnoDB底层原理 +- 索引 +- 索引优化! + + + +## 12、动态 SQL + +对应模块`mybatis-08` + +**什么是动态SQL:动态SQL就是指根据不同的条件生成不同的SQL语句** + +利用动态 SQL 这一特性可以彻底摆脱这种痛苦。 + +动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。 + +```xml +if +choose (when, otherwise) +trim (where, set) +foreach +``` + + + +### 搭建环境 + +```sql +CREATE TABLE `blog` ( + `id` varchar(50) NOT NULL COMMENT '博客id', + `title` varchar(100) NOT NULL COMMENT '博客标题', + `author` varchar(30) NOT NULL COMMENT '博客作者', + `create_time` datetime NOT NULL COMMENT '创建时间', + `views` int(30) NOT NULL COMMENT '浏览量' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 +``` + + + +创建一个基础工程 + +1. 导包 + +2. 编写配置文件 + +3. 工具包 + + ```java + //抑制警告 + @SuppressWarnings("all") + public class IDutils { + public static String getId(){ + return UUID.randomUUID().toString().replace("-",""); + } + } + ``` + +4. 编写实体类 + + ```java + @Data + public class Blog { + private String id; + private String title; + private String author; + private Date createTime; + private int views; + } + ``` + + +插入数据 + +```java + @Test + public void addInitBlog(){ + SqlSession sqlSession = MybatisUtils.getSqlSession(); + BlogMapper mapper = sqlSession.getMapper(BlogMapper.class); + Blog blog = new Blog(IDutils.getId(),"Mybatis如此简单","狂神说",new Date(),9999); + mapper.addBlog(blog); + blog = new Blog(IDutils.getId(),"Java如此简单","狂神说",new Date(),1000); + mapper.addBlog(blog); + blog = new Blog(IDutils.getId(),"Spring如此简单","狂神说",new Date(),9999); + mapper.addBlog(blog); + blog = new Blog(IDutils.getId(),"微服务如此简单","狂神说",new Date(),9999); + mapper.addBlog(blog); + sqlSession.close(); + } +``` + + + +5. 编写实体类对应Mapper接口 和 Mapper.XML文件 + + + +### IF + +xml + +```xml + +``` + +代码: +```java +@Test +public void queryBlogIF() { + SqlSession sqlSession = MybatisUtils.getSqlSession(); + BlogMapper mapper = sqlSession.getMapper(BlogMapper.class); + HashMap map = new HashMap(); +// map.put("title","Mybatis如此简单"); + map.put("author","狂神说"); + List blogList = mapper.queryBlogIF(map); + for (Blog blog : blogList) { + System.out.println(blog); + } + + sqlSession.close(); +} +``` + +### choose (when, otherwise) + +类似于java里的switch + + + +```xml + +``` + +代码 + +```java +@Test +public void queryBlogChoose(){ + SqlSession sqlSession = MybatisUtils.getSqlSession(); + BlogMapper mapper = sqlSession.getMapper(BlogMapper.class); + HashMap map = new HashMap(); + map.put("title","Mybatis如此简单2"); +// map.put("author","狂神说"); + map.put("id","5d3adbfea47b4493bc086cf8dbb8998a"); + mapper.updateBlog(map); + + sqlSession.close(); +} +``` + +### trim (where,set) + +```xml +select * from mybatis.blog + + + title = #{title} + + + and author = #{author} + + +``` + +```xml + + update mybatis.blog + + + title = #{title}, + + + author = #{author} + + + where id = #{id} + +``` + +代码: + +```java +@Test +public void updateBlog(){ + SqlSession sqlSession = MybatisUtils.getSqlSession(); + BlogMapper mapper = sqlSession.getMapper(BlogMapper.class); + HashMap map = new HashMap(); +// map.put("title","Mybatis如此简单"); +// map.put("author","狂神说"); + List blogList = mapper.queryBlogChoose(map); + for (Blog blog : blogList) { + System.out.println(blog); + } + sqlSession.close(); +} +``` + +**所谓的动态SQL,本质还是SQL语句 , 只是我们可以在SQL层面,去执行一个逻辑代码** + +if + +where , set , choose ,when + + + + + +### SQL片段 + +有的时候,我们可能会将一些功能的部分抽取出来,方便复用! + +1. 使用SQL标签抽取公共的部分 + + ```xml + + + title = #{title} + + + and author = #{author} + + + ``` + +2. 在需要使用的地方使用Include标签引用即可 + + ```xml + + ``` + + + +注意事项: + +- 最好基于单表来定义SQL片段! +- 不要存在where标签 + + + +### Foreach + +```sql +select * from user where 1=1 and + + + #{id} + + +(id=1 or id=2 or id=3) + +``` + +![1569979229205](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569979229205.png) + +![1569979339190](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569979339190.png) + +```xml + + + +``` + +最后sql为: select * from mybatis.blog WHERE ( id = ? or id = ? or id = ? ) + +代码: + +```java +@Test +public void queryBlogForeach(){ + SqlSession sqlSession = MybatisUtils.getSqlSession(); + BlogMapper mapper = sqlSession.getMapper(BlogMapper.class); + HashMap map = new HashMap(); + + ArrayList ids = new ArrayList(); + ids.add("d97ca9c234df463e950f252d22fb5f85"); + ids.add("4cfe16fcebb145f894b6ec9033f8ae33"); + ids.add("5d3adbfea47b4493bc086cf8dbb8998a"); + + map.put("ids",ids); + + List blogList = mapper.queryBlogForeach(map); + for (Blog blog : blogList) { + System.out.println(blog); + } + sqlSession.close(); +} +``` + + + +动态SQL就是在拼接SQL语句,我们只要保证SQL的正确性,按照SQL的格式,去排列组合就可以了 + +建议: + +- 现在Mysql中写出完整的SQL,再对应的去修改成为我们的动态SQL实现通用即可! + + + +## 13、缓存 + +对应模块`mybatis-09` + +### 13.1、简介 + +我们每次查询数据的时候,是通过数据库查询。在数据需要大量查询时,我们就需要用到缓存,下载查询相同的时候,通过内存查询,不需要经过数据库,大大的节省的资源 + +1. 什么是缓存 [ Cache ]? + - 存在内存中的临时数据。 + - 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。 +2. 为什么使用缓存? + + - 减少和数据库的交互次数,减少系统开销,提高系统效率。 +3. 什么样的数据能使用缓存? + + - 经常查询并且不经常改变的数据。【可以使用缓存】 + + +我们再次查询相同数据的时候,直接走缓存,就不用走数据库了 + +### 13.2、Mybatis缓存 + +- MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。 +- MyBatis系统中默认定义了两级缓存:**一级缓存**和**二级缓存** + - 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存) + + - 二级缓存需要手动开启和配置,他是基于namespace级别的缓存。 + + - 为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存 + + + +### 13.3、一级缓存 + +- 一级缓存也叫本地缓存: SqlSession + - 与数据库同一次会话期间查询到的数据会放在本地缓存中。 + - 以后如果需要获取相同的数据,直接从缓存中拿,没必须再去查询数据库; + + + +测试步骤: + +1. 开启日志! +2. 测试在一个Sesion中查询两次相同记录 +3. 查看日志输出 + +![1569983650437](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569983650437.png) + + + +缓存失效的情况: + +1. 查询不同的东西 + +2. 增删改操作,可能会改变原来的数据,所以必定会刷新缓存! + + ![1569983952321](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569983952321.png) + +3. 查询不同的Mapper.xml + +4. 手动清理缓存! + + ![1569984008824](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569984008824.png) + + + +小结:一级缓存默认是开启的,只在一次SqlSession中有效,也就是拿到连接到关闭连接这个区间段! + +一级缓存就是一个Map。 + + + + +### 13.4、二级缓存 + +- 二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存 +- 基于namespace级别的缓存,一个名称空间,对应一个二级缓存; +- 工作机制 + - 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中; + - 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中; + - 新的会话查询信息,就可以从二级缓存中获取内容; + - 不同的mapper查出的数据会放在自己对应的缓存(map)中; + + + +步骤: + +1. 开启全局缓存 + + ```xml + + + ``` + +2. 在要使用二级缓存的Mapper中开启 + + ```xml + + + ``` + + 也可以自定义参数 + + ```xml + + + ``` + +3. 测试 + + 1. 问题:我们需要将实体类序列化!否则就会报错! + + ``` + Caused by: java.io.NotSerializableException: com.kuang.pojo.User + ``` + + + +小结: + +- 只要开启了二级缓存,在同一个Mapper下就有效 +- 所有的数据都会先放在一级缓存中; +- 只有当会话提交,或者关闭的时候,才会提交到二级缓冲中! + + + + + +### 13.5、缓存原理 + +![1569985541106](https://cdn.jsdelivr.net/gh/oddfar/static/img/Mybatis课堂笔记.assets//1569985541106.png) + + + +### 13.6、自定义缓存-ehcache + +```xml +Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存 +``` + +要在程序中使用ehcache,先要导包! + +```xml + + + org.mybatis.caches + mybatis-ehcache + 1.1.0 + +``` + +在mapper中指定使用我们的ehcache缓存实现! + +```xml + + +``` + +ehcache.xml + +```xml + + + + + + + + + + + + + +``` + + + + + + + + + + + + + + + + + + + diff --git "a/docs/04.\346\241\206\346\236\266/02.mybatis-plus/01.\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/docs/04.\346\241\206\346\236\266/02.mybatis-plus/01.\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 00000000..8b7243f2 --- /dev/null +++ "b/docs/04.\346\241\206\346\236\266/02.mybatis-plus/01.\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,792 @@ +--- +title: mybatis-plus学习笔记 +permalink: /mybatis-plus/study-note +date: 2021-05-09 13:35:32 +--- + +# MyBatisPlus + + + + + +- [概序](#%E6%A6%82%E5%BA%8F) +- [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B) +- [配置日志](#%E9%85%8D%E7%BD%AE%E6%97%A5%E5%BF%97) +- [CRUD扩展](#crud%E6%89%A9%E5%B1%95) + - [插入操作](#%E6%8F%92%E5%85%A5%E6%93%8D%E4%BD%9C) + - [**主键生成策略**](#%E4%B8%BB%E9%94%AE%E7%94%9F%E6%88%90%E7%AD%96%E7%95%A5) + - [更新操作](#%E6%9B%B4%E6%96%B0%E6%93%8D%E4%BD%9C) + - [自动填充](#%E8%87%AA%E5%8A%A8%E5%A1%AB%E5%85%85) + - [乐观锁](#%E4%B9%90%E8%A7%82%E9%94%81) + - [查询操作](#%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C) + - [分页查询](#%E5%88%86%E9%A1%B5%E6%9F%A5%E8%AF%A2) + - [删除操作](#%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C) + - [逻辑删除](#%E9%80%BB%E8%BE%91%E5%88%A0%E9%99%A4) + - [性能分析插件](#%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E6%8F%92%E4%BB%B6) + - [条件构造器](#%E6%9D%A1%E4%BB%B6%E6%9E%84%E9%80%A0%E5%99%A8) +- [代码自动生成器](#%E4%BB%A3%E7%A0%81%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90%E5%99%A8) + + + + + +

狂神mybatis-plus视频教程:https://www.bilibili.com/video/BV17E411N7KN

+ +## 概序 + +原先写crud,需要在xml配置或注解中写sql语句,用了MyBatisPlus后,对单表进行crud无需再写sql语句 + +简单的增删改查还行,不支持多表操作,一般不在公司里使用,适合个人快速开发和偷懒使用 + +官网:https://baomidou.com/ + +简介:简化mybatis,简化开发、提高写代码效率 + + + +- **无侵入**:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑 +- **损耗小**:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作 +- **强大的 CRUD 操作**:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求 +- **支持 Lambda 形式调用**:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错 +- **支持主键自动生成**:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题 +- **支持 ActiveRecord 模式**:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作 +- **支持自定义全局通用操作**:支持全局通用方法注入( Write once, use anywhere ) +- **内置代码生成器**:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用 +- **内置分页插件**:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询 +- **分页插件支持多种数据库**:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库 +- **内置性能分析插件**:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询 +- **内置全局拦截插件**:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作 + +## 快速开始 + +官网地址:https://mp.baomidou.com/guide/quick-start.html + +使用第三方组件: + +1. 导入对应的依赖 + +2. 研究依赖如何配置 + +3. 代码如何编写 + +4. 提高扩展技术能力! + +步骤: + +1、创建数据库 `mybatis_plus` + +2、创建user表并插入数据 + +```sql +DROP TABLE IF EXISTS user; + +CREATE TABLE user +( + id BIGINT(20) NOT NULL COMMENT '主键ID', + name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', + age INT(11) NULL DEFAULT NULL COMMENT '年龄', + email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', + PRIMARY KEY (id) +); + +DELETE FROM user; + +INSERT INTO user (id, name, age, email) VALUES +(1, 'Jone', 18, 'test1@baomidou.com'), +(2, 'Jack', 20, 'test2@baomidou.com'), +(3, 'Tom', 28, 'test3@baomidou.com'), +(4, 'Sandy', 21, 'test4@baomidou.com'), +(5, 'Billie', 24, 'test5@baomidou.com'); +``` + +3、编写项目,初始化项目!使用SpringBoot初始化! + +4、导入依赖 + +```xml + + + mysql + mysql-connector-java + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + 3.4.2 + +``` + +5、填写properties配置 + +```properties +spring.datasource.username=root +spring.datasource.password=123456 +spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +``` + +6、填写测试类,读取数据库 + +原先我们使用mybatis:pojo-dao(连接mybatis,配置mapper.xml文件)-service-controller + +现在: + +- pojo类 + + ```java + @Data + @NoArgsConstructor + @AllArgsConstructor + public class User { + private Long id; + private String name; + private Integer age; + private String email; + } + ``` + +- mapper接口 + + ```java + // 在对应的Mapper上面继承基本的类 BaseMapper + @Repository // 代表持久层 + public interface UserMapper extends BaseMapper { + // 所有的CRUD操作都已经编写完成了 + // 你不需要像以前的配置一大堆文件了! + } + ``` + +- springboot启动类 + + 添加注解来扫描 + + ```java + @MapperScan("com.oddfar.mapper") + ``` + +- 测试类 + + ```java + // 继承了BaseMapper,所有的方法都来自己父类 + // 我们也可以编写自己的扩展方法! + @Autowired + private UserMapper userMapper; + + @Test + void contextLoads() { + // 参数是一个 Wrapper ,条件构造器,这里我们先不用,填写null + // 查询全部用户 + List users = userMapper.selectList(null); + users.forEach(System.out::println); + } + ``` + + + +## 配置日志 + +我们所有的sql现在是不可见的,我们希望知道他是怎么执行的,所以我们必须要看日志! + +```properties +# 配置日志 +mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl +``` + +## CRUD扩展 + +创建controller包,编写UserController类 + +### 插入操作 + +```java +@Test +void addUser() { + User user = new User(); + user.setName("zhiyuan"); + user.setAge(3); + user.setEmail("123456@qq.com"); + + // 帮我们自动生成id + int result = userMapper.insert(user); + System.out.println(result); // 受影响的行数 + System.out.println(user); // 发现,id会自动回填 +} +``` + +数据库插入的id的默认值为:全局的唯一id + +### **主键生成策略** + +> 默认 ID_WORKER 全局唯一id +> 分布式系统唯一id生成:https://www.cnblogs.com/haoxinyue/p/5208136.html + +**雪花算法:** + +snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。可以保证几乎全球唯一! + +**主键自增:** + +实体类字段上 @TableId(type = IdType.AUTO) + +![image-20210405172103065](https://cdn.jsdelivr.net/gh/oddfar/static/img/mybatis-plus.assets/image-20210405172103065.png) + +数据库id字段自增! + +![image-20210405172141349](https://cdn.jsdelivr.net/gh/oddfar/static/img/mybatis-plus.assets/image-20210405172141349.png) + +关于注解的介绍:https://baomidou.com/guide/annotation.html#tableid + +### 更新操作 + +```java + /** + * 更改一个用户 + */ + @Test + void testUpdate() { + User user = new User(); + // 通过条件自动拼接动态sql + user.setId(5L); + user.setName("test"); + user.setAge(5); + // 注意:updateById 但是参数是一个 对象! + int i = userMapper.updateById(user); + System.out.println(i); + } +``` + +所有的sql都是自动帮你动态配置的! + +### 自动填充 + +官网:https://baomidou.com/guide/auto-fill-metainfo.html + +创建时间、修改时间!这些个操作一遍都是自动化完成的,我们不希望手动更新! + +阿里巴巴开发手册:所有的数据库表:gmt_create、gmt_modified几乎所有的表都要配置上!而且需 要自动化! + +**方式一:数据库级别(工作中一般不允许你修改数据库)** + +1、在表中新增字段 create_time, update_time + +```sql +ALTER TABLE `mybatis_plus`.`user` + ADD COLUMN `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP NULL COMMENT '创建时间' AFTER `email`, + ADD COLUMN `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NULL COMMENT '更新时间' AFTER `create_time`; +``` + +![image-20210405173344140](https://cdn.jsdelivr.net/gh/oddfar/static/img/mybatis-plus.assets/image-20210405173344140.png) + +默认:CURRENT_TIMESTAMP + +2、再次测试插入方法,我们需要先把实体类同步! + +**方式二:代码级别** + +1、删除数据库的默认值、更新操作! + +![image-20210405173548133](https://cdn.jsdelivr.net/gh/oddfar/static/img/mybatis-plus.assets/image-20210405173548133.png) + +2、实体类字段属性上需要增加注解 + +```java +// 字段添加填充内容 +@TableField(fill = FieldFill.INSERT) +private Date createTime; +@TableField(fill = FieldFill.INSERT_UPDATE) +private Date updateTime; +``` + +3、编写处理器来处理这个注解即可! + +```java +package com.oddfar.handler; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Slf4j +@Component // 一定不要忘记把处理器加到IOC容器中! +public class MyMetaObjectHandler implements MetaObjectHandler { + // 插入时的填充策略 + @Override + public void insertFill(MetaObject metaObject) { + log.info("start insert fill....."); + // setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject + this.setFieldValByName("createTime", new Date(), metaObject); + this.setFieldValByName("updateTime", new Date(), metaObject); + } + + // 更新时的填充策略 + @Override + public void updateFill(MetaObject metaObject) { + log.info("start update fill....."); + this.setFieldValByName("updateTime", new Date(), metaObject); + } +} +``` + +4、测试插入 ,测试更新、观察时间即可! + + + +### 乐观锁 + +在面试过程中,我们经常会被问道乐观锁,悲观锁!这个其实非常简单! + +> 乐观锁:顾名思义十分乐观,他总是认为不会出现问题,无论干什么不去上锁!如果出现了问题,再次更新值测试! +> +> 悲观锁:顾名思义十分悲观,他总是任务总是出现问题,无论干什么都会上锁!再去操作! + +当要更新一条记录的时候,希望这条记录没有被别人更新 +乐观锁实现方式: + +乐观锁实现方式: + +- 取出记录,获取当前version +- 更新时,带上这个version +- 执行更新时,set version = new version where version = oldversion +- 如果version不对,就更新失败 + + + +**测试一下mybatis-plus的乐观锁插件** + +官方文档(必看):https://baomidou.com/guide/interceptor.html + +有的方法会过期,具体操作以官方文档例子为主 + + + +1、给数据库中增加version字段! + +![image-20210405175422788](https://cdn.jsdelivr.net/gh/oddfar/static/img/mybatis-plus.assets/image-20210405175422788.png) + +2、我们实体类加对应的字段 + +```java +@Version //乐观锁Version注解 +private Integer version; +``` + +3、注册组件 + +```java +package com.oddfar.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author zhiyuan + * @date 2021/4/5 17:57 + */ +// 扫描我们的 mapper 文件夹 +@MapperScan("com.oddfar.mapper") +@Configuration // 配置类 +public class MyBatisPlusConfig { + + // 注册乐观锁和分页插件(新版:3.4.0) + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor(){ + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 乐观锁插件 + return interceptor; + } + +} +``` + +旧版如下: + +```java +@Bean +public OptimisticLockerInterceptor OptimisticLockerInterceptor() { + return new OptimisticLockerInterceptor(); +} +``` + +4、测试一下! + +```java +/** + * 测试乐观锁 + */ +@Test +public void testOptimisticLocker(){ + // 1、查询用户信息 + User user = userMapper.selectById(1L); + + // 2、修改用户信息 + user.setName("zhiyuan"); + user.setEmail("123456@qq.com"); + // 3、执行更新操作 + userMapper.updateById(user); +} +``` + +我们来模拟下 + +```java +// 测试乐观锁失败!多线程下 +@Test +public void testOptimisticLocker2(){ + // 线程 1 + User user = userMapper.selectById(1L); + user.setName("zhiyuan000000"); + user.setEmail("000000000@qq.com"); + // 模拟另外一个线程执行了插队操作 + User user2 = userMapper.selectById(1L); + user2.setName("zhiyuan1111111"); + user2.setEmail("111111111@qq.com"); + userMapper.updateById(user2); + // 自旋锁来多次尝试提交! + userMapper.updateById(user); // 如果没有乐观锁就会覆盖插队线程的值! +} +``` + +![image-20210405184334804](https://cdn.jsdelivr.net/gh/oddfar/static/img/mybatis-plus.assets/image-20210405184334804.png) + +如果不加乐观锁,以上代码,name最后就是`zhiyuan000000` + + + +### 查询操作 + +```java +// 指定id查询 +@Test +public void testSelectById(){ + User user = userMapper.selectById(1L); + System.out.println(user); +} +// 多个id批量查询! +@Test +public void testSelectByBatchId(){ + List users = userMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L)); + users.forEach(System.out::println); +} +// 多个where条件查询之一使用map操作 +@Test +public void testSelectByBatchIds(){ + HashMap map = new HashMap<>(); + // 自定义要查询 + map.put("name","zhiyuan"); + map.put("age",3); + List users = userMapper.selectByMap(map); + users.forEach(System.out::println); +} +``` + +### 分页查询 + +分页在网站使用的十分之多! + +1、原始的 limit 进行分页 +2、pageHelper 第三方插件 +3、MP其实也内置了分页插件! + + + +**mybais-plus分页查询方法:** + +1、配置拦截器组件即可 + +```java +interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));//分页插件 +``` + +旧版如下: + +```java +//分页插件 +@Bean +public PaginationInterceptor paginationInterceptor() { + PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); + return paginationInterceptor; +} +``` + +2、直接使用Page对象即可! + +```java +/** + * 测试分页查询 + */ +@Test +public void testPage() { + //参数一:当前页 + //参数二:页面大小 + Page page = new Page<>(1, 5); + userMapper.selectPage(page, null); + page.getRecords().forEach(System.out::println); + System.out.println(page.getTotal());//总数量 + +} +``` + +### 删除操作 + +```java +// 通过id单个删除 +@Test +public void testDeleteById(){ + userMapper.deleteById(1379026108137123842L); +} +// 通过id批量删除 +@Test +public void testDeleteBatchId(){ + userMapper.deleteBatchIds(Arrays.asList(1379026108137123842L,1379026441420791810L)); +} +// 通过map删除 +@Test +public void testDeleteMap(){ + HashMap map = new HashMap<>(); + map.put("name","zhiyuan4"); + userMapper.deleteByMap(map); +} +``` + +### 逻辑删除 + +官方文档:https://baomidou.com/guide/logic-delete.html + +物理删除 :从数据库中直接移除 + +逻辑删除 :再数据库中没有被移除,而是通过一个变量来让他失效! +比如一个系统,有管理员,操作员,用户等...... +设置数据库的时候,加个`deleted`字段,默认为0,代表数据存在 +当用户或操作员要删除数据了,我们则把`deleted`赋值为1,表示数据已删除 +这样管理员则可以在后台查询被删除的记录,防止数据的丢失,类似于回收站! + +方法如下: + +1、在数据表中增加一个 deleted 字段 + +![image-20210405235056518](https://cdn.jsdelivr.net/gh/oddfar/static/img/mybatis-plus.assets/image-20210405235056518.png) + +2、实体类中增加属性 + +```java +@TableLogic //逻辑删除 +private Integer deleted; +``` + +3、配置 + +application.yml(properties也可以) + +```yaml +mybatis-plus: + global-config: + db-config: + logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + +``` + +4、测试 + +![image-20210406001501121](https://cdn.jsdelivr.net/gh/oddfar/static/img/mybatis-plus.assets/image-20210406001501121.png) + +记录依旧在数据库,但是值确已经变化了! + + + +### 性能分析插件 + + + +作用:性能分析拦截器,用于输出每条 SQL 语句及其执行时间。MP也提供性能分析插件,如果超过这个时间就停止运行,不过新版本已去掉这功能。 + +推荐使用druid + +或者使用P6Spy: + +> [SpringBoot - MyBatis-Plus使用详解18(结合P6Spy进行SQL性能分析)](https://www.hangge.com/blog/cache/detail_2928.html) + + + +### 条件构造器 + + + +官网文档:https://baomidou.com/guide/wrapper.html + +wrapper:十分重要: 我们写一些复杂的sql就可以使用它来替代! + +1、查询name不为空的用户,并且邮箱不为空的用户,年龄大于12 + +```java +@Test +void contextLoads() { + //查询name不为空的用户,并且邮箱不为空的用户,年龄大于12 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.isNotNull("name") + .isNotNull("email") + .ge("age", 12); + userMapper.selectList(wrapper).forEach(System.out::println); +} +``` + +2、查询名字为zhiyuan2 + +```java +@Test +void test2(){ + //查询名字为zhiyuan2 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("name", "zhiyuan2"); + User user = userMapper.selectOne(wrapper); + System.out.println(user); +} +``` + +3、查询年龄在19到30岁之间的用户 + +```java +@Test +void test3(){ + //查询年龄在19到30岁之间的用户 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.between("age", 19, 30); //区间 + Integer count = userMapper.selectCount(wrapper); + System.out.println(count); +} +``` + +4、查询name不包含t,邮箱以123开头 + +- 例: `notLike("name", "王")`--->`name not like '%王%'` +- 例: `likeRight("name", "王")`--->`name like '王%'` + +```java +//模糊查询 +@Test +void test4(){ + //查询name不包含t,邮箱以123开头 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.notLike("name", "t") + .likeRight("email", "123"); + List> maps = userMapper.selectMaps(wrapper); + maps.forEach(System.out::println); +} +``` + +5、子查询 + +```java +@Test +void test5(){ + QueryWrapper wrapper = new QueryWrapper<>(); + //id 在子查询中查出来 + wrapper.inSql("id", "select id from user where id < 3"); + List objects = userMapper.selectObjs(wrapper); + objects.forEach(System.out::println); +} +``` + +最后sql: + +> SELECT id,name,age,email,version,deleted,create_time,update_time FROM user WHERE deleted=0 AND (id IN (select id from user where id < 3)) + +6、通过id进行排序--降序 + +```java +@Test +void test6(){ + QueryWrapper wrapper = new QueryWrapper<>(); + //通过id进行排序--降序 + wrapper.orderByDesc("id"); + List users = userMapper.selectList(wrapper); + users.forEach(System.out::println); +} +``` + +## 代码自动生成器 + +dao、pojo、service、controller都给我自己去编写完成! + +AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。 + +测试: + +```java +public class Code { + public static void main(String[] args) { + //需要构建一个 代码自动生成器 对象 + // 代码生成器 + AutoGenerator mpg = new AutoGenerator(); + //配置策略 + + //1、全局配置 + GlobalConfig gc = new GlobalConfig(); + String projectPath = System.getProperty("user.dir"); + gc.setOutputDir(projectPath + "/src/main/java"); + gc.setAuthor("ChanV"); + gc.setOpen(false); + gc.setFileOverride(false); //是否覆盖 + gc.setServiceName("%sService"); //去Service的I前缀 + gc.setIdType(IdType.ID_WORKER); + gc.setDateType(DateType.ONLY_DATE); + gc.setSwagger2(true); + mpg.setGlobalConfig(gc); + + //2、设置数据源 + DataSourceConfig dsc = new DataSourceConfig(); + dsc.setUrl("jdbc:mysql://localhost:3306/mybatis-plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8"); + dsc.setDriverName("com.mysql.cj.jdbc.Driver"); + dsc.setUsername("root"); + dsc.setPassword("root"); + dsc.setDbType(DbType.MYSQL); + mpg.setDataSource(dsc); + + //3、包的配置 + PackageConfig pc = new PackageConfig(); + pc.setModuleName("blog"); + pc.setParent("com.chanv"); + pc.setEntity("pojo"); + pc.setMapper("mapper"); + pc.setService("service"); + pc.setController("controller"); + mpg.setPackageInfo(pc); + + //4、策略配置 + StrategyConfig strategy = new StrategyConfig(); + strategy.setInclude("user"); //设置要映射的表名 + strategy.setNaming(NamingStrategy.underline_to_camel); + strategy.setColumnNaming(NamingStrategy.underline_to_camel); + strategy.setEntityLombokModel(true); //自动lombok + strategy.setLogicDeleteFieldName("deleted"); + //自动填充配置 + TableFill createTime = new TableFill("create_time", FieldFill.INSERT); + TableFill updateTime = new TableFill("update_time", FieldFill.UPDATE); + ArrayList tableFills = new ArrayList<>(); + tableFills.add(createTime); + tableFills.add(updateTime); + strategy.setTableFillList(tableFills); + //乐观锁 + strategy.setVersionFieldName("version"); + strategy.setRestControllerStyle(true); + strategy.setControllerMappingHyphenStyle(true); //localhost:8080/hello_id_2 + mpg.setStrategy(strategy); + + mpg.execute(); //执行代码构造器 + } +} +``` + +![在这里插入图片描述](https://cdn.jsdelivr.net/gh/oddfar/static/img/mybatis-plus.assets/20201122104625171.png) \ No newline at end of file diff --git "a/docs/04.\346\241\206\346\236\266/03.spring/01.\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/docs/04.\346\241\206\346\236\266/03.spring/01.\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 00000000..cdca2aa3 --- /dev/null +++ "b/docs/04.\346\241\206\346\236\266/03.spring/01.\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,16 @@ +--- +title: spring学习笔记 +permalink: /spring/study-note +date: 2021-05-12 22:31:24 +--- + +# Spring + +

狂神java系列视频教程:https://space.bilibili.com/95256449/

+ +## 1、Spring概述 + +官网 : http://spring.io/ + + + diff --git "a/docs/10.\345\205\263\344\272\216/01.\345\205\263\344\272\216 - \346\210\221/01.\345\205\263\344\272\216 - \346\210\221.md" "b/docs/10.\345\205\263\344\272\216/01.\345\205\263\344\272\216 - \346\210\221/01.\345\205\263\344\272\216 - \346\210\221.md" new file mode 100644 index 00000000..504b2ce2 --- /dev/null +++ "b/docs/10.\345\205\263\344\272\216/01.\345\205\263\344\272\216 - \346\210\221/01.\345\205\263\344\272\216 - \346\210\221.md" @@ -0,0 +1,56 @@ +--- +title: 关于 - 我 +date: 2021-05-16 22:53:19 +permalink: /about/me/ +--- + + + + + +- [关于我](#%E5%85%B3%E4%BA%8E%E6%88%91) +- [网站初衷](#%E7%BD%91%E7%AB%99%E5%88%9D%E8%A1%B7) +- [学习资料](#%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99) + + + +## 关于我 + + + +一个在校的垃圾大二学生 + +和我同学 `李祖泽` 一起写此网站笔记 + + + +## 网站初衷 + + + +- 好记性不如烂笔头 + + 学的再快再多,不如学好学精 + +- 监督自己的学习 + + 本人学习很不爱记笔记,学过之后,很多知识忘了。以此网站监督自己学习! + +- 帮助其他初学者 + + 笔记内容不深,适合初学者阅读 + + + +## 学习资料 + +本人学习java主要来源于尚硅谷、狂神、黑马程序员的教程 + +视频可在bilibili上搜 + +本人处于学习阶段,学习java也没多长时间,没硬实力写出完好的自己的知识体系 + +大部分java笔记借助于狂神和尚硅谷课堂笔记,在文章结尾或开头有视频教程链接地址传送 + +笔记加了点自己的理解,和其他站点的讲解。 + diff --git "a/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/02.\346\226\207\346\241\243\347\232\204\346\220\255\345\273\272\345\217\212\345\206\231\344\275\234.md" "b/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/02.\346\226\207\346\241\243\347\232\204\346\220\255\345\273\272\345\217\212\345\206\231\344\275\234.md" new file mode 100644 index 00000000..0848a283 --- /dev/null +++ "b/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/02.\346\226\207\346\241\243\347\232\204\346\220\255\345\273\272\345\217\212\345\206\231\344\275\234.md" @@ -0,0 +1,181 @@ +--- +title: 文档的搭建及写作 +date: 2021-05-16 22:47:39 +permalink: /about/blog/2 +--- + + + + + + + +- [准备](#%E5%87%86%E5%A4%87) +- [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B) +- [写作](#%E5%86%99%E4%BD%9C) + - [图片](#%E5%9B%BE%E7%89%87) + - [文件命名约定](#%E6%96%87%E4%BB%B6%E5%91%BD%E5%90%8D%E7%BA%A6%E5%AE%9A) + - [级别说明](#%E7%BA%A7%E5%88%AB%E8%AF%B4%E6%98%8E) +- [配置信息修改](#%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF%E4%BF%AE%E6%94%B9) +- [详情请看](#%E8%AF%A6%E6%83%85%E8%AF%B7%E7%9C%8B) + + + + + +本站 github 地址:[https://github.com/oddfar/docs](https://github.com/oddfar/docs) + +使用 [vuepress](https://vuepress.vuejs.org/zh) 搭建,自动部署在[GitHub Pages](https://pages.github.com/) + +使用 [vdoing ](https://github.com/xugaoyi/vuepress-theme-vdoing)主题 + +纯新手可看 bilibili 视频j教程:[https://www.bilibili.com/video/BV17t41177cr](https://www.bilibili.com/video/BV17t41177cr) + +## 准备 + +VuePress 需要 [Node.js ](https://nodejs.org/en/)>= 8.6 + +1. 克隆到本地并进入目录 + + ``` + git clone https://github.com/oddfar/docs.git && cd docs + ``` + +2. 安装本地依赖 + + ``` + npm install + ``` + +3. 本地测试 + + ``` + npm run dev + ``` + + 默认访问链接:http://localhost:8080/doc + +## 快速开始 + +使用`markdown`语法编写`md`文件,所有笔记`md`文件放在`docs/docs`目录下 + +例如添加`test`类,并编写`hello.md`文件 + +1. 创建目录 + + 格式:序号+标题 + + 例如:30.test + +2. 添加笔记 + + 例如:01.hello.md + +3. 编写内容 + + ``` + --- + title: 笔记标题 + permalink: /test/hello/ + date: 2021-01-01 01:01:01 + --- + + ## 标题 + + hello world + ``` + + tittle:标题,不填写则默认文件名中的标题,即`hello` + + permalink:访问链接,不填写则自动生成 + + date:日期,默认文件创建时间 + +4. 测试运行 + + 在项目根目录下 + + ``` + npm run dev + ``` + +## 写作 + +推荐写 `markdown` 软件 + +1. Typora + + 下载即用 + +2. vs code + + 需要下载 markdown 插件 + + 里面终端可设置 `git bash` 终端,方便调试,适合程序员使用 + + 设置教程:[新版本VS Code 终端设置为git bash](https://blog.csdn.net/A_zhiyuan/article/details/116930325) + + + +### 图片 + +图片地址必须是可在线访问的链接,不能是本地图片 + +**如何免费搭建图床,并在markdown中使用教程:[https://oddfar.com/archives/91/](https://oddfar.com/archives/91/)** + + + +### 文件命名约定 + +- 无论是**文件**还是**文件夹**,请为其名称添加上正确的**正整数序号**和`.`,从`00`或`01`开始累计,如`01.文件夹`、`02.文件.md`,我们将会按照序号的顺序来决定其在侧边栏当中的顺序。 +- 同一级别目录别内即使只有一个文件或文件夹也要为其加上序号。 +- 文件或文件夹名称中间不能出现多余的点`.`,如`01.我是.名称.md`中间出现`.`将会导致解析错误。 + +序号只是用于决定先后顺序,并不一定需要连着,如`01、02、03...`,实际工作中可能会在两个文章中间插入一篇新的文章,因此为了方便可以采用间隔序号`10、20、30...`,后面如果需要在`10`和`20`中间插入一篇新文章,可以给定序号`15`。 + +### 级别说明 + +源目录(一般是`docs`)底下的级别现在我们称之为`一级目录`,`一级目录`的下一级为`二级目录`,以此类推,最多到`四级目录`。 + +- 一级目录 + 1. `.vuepress`、`@pages`、`_posts`、`index.md 或 README.md` 这些文件(文件夹)不参与数据生成。 + 2. 序号非必须。(如一些专栏,可以不用序号) +- 二级目录 + 1. 该级别下可以同时放文件夹和`.md`文件,但是两者序号要连贯(参考下面的例子中的`其他`)。 + 2. 必须有序号 +- 三级目录 + - (同上) +- 四级目录 + 1. 该级别下只能放`.md`文件。 + 2. 必须有序号 + +所有级别内至少有一个文件或文件夹。 + + + +vdoing主题介绍文档:https://doc.xugaoyi.com/pages/33d574/ + +## 配置信息修改 + +一些常用的 + +目录 `docs\.vuepress` 下 + +- config.js + + 修改 vuepress 和主题一些配置 + +- config\nav.js + + 修改首页导航栏 + + + +## 详情请看 + +关于 vuepress 的配置: + +- [vuepress官方文档](https://vuepress.vuejs.org/zh) + ++ [vdoing主题介绍文档](https://doc.xugaoyi.com/) + diff --git "a/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/05.\346\226\207\346\241\243\347\232\204\351\203\250\347\275\262.md" "b/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/05.\346\226\207\346\241\243\347\232\204\351\203\250\347\275\262.md" new file mode 100644 index 00000000..a48048d3 --- /dev/null +++ "b/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/05.\346\226\207\346\241\243\347\232\204\351\203\250\347\275\262.md" @@ -0,0 +1,111 @@ +--- +title: 文档的部署 +date: 2021-05-16 22:53:51 +permalink: /about/blog/5 +--- + + + + + +- [手动部署](#%E6%89%8B%E5%8A%A8%E9%83%A8%E7%BD%B2) + - [github](#github) + - [自己服务器](#%E8%87%AA%E5%B7%B1%E6%9C%8D%E5%8A%A1%E5%99%A8) +- [github自动部署](#github%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2) + - [生成Token](#%E7%94%9F%E6%88%90token) + - [配置秘钥](#%E9%85%8D%E7%BD%AE%E7%A7%98%E9%92%A5) + + + + + +## 手动部署 + +### github + +创建分支:`gh-pages` + +更改文件`deploy.sh`内容 + +``` +githubUrl=git@github.com:oddfar/docs.git +``` + +``` +githubUrl=https://oddfar:${GITHUB_TOKEN}@github.com/oddfar/docs.git +``` + +双击运行`deploy.sh` + +之后配置 [GitHub Pages](https://pages.github.com/) + +![image-20210517151354287](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210517151356.png) + + + +### 自己服务器 + +根目录下执行命令 + +```sh +npm run build +``` + +生成文件在 `docs\.vuepress\dist\` 目录下 + +打包到服务器即可 + +注:本地不可直接访问,需要配合插件,详情看官方文档 + +## github自动部署 + +目录 ` .github\workflows\` 下的 `ci.yml` 文件为配置文件 + +配置文件已经写好了,我们只需要在 github 上配置下秘钥(secrets) + +前提已经配置了 [GitHub Pages](https://pages.github.com/) 服务并能正常访问 + +### 生成Token + +**Settings -> Developer settings->Personal access tokens** + +1、Settings + +![image-20210517142414602](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210517142418.png) + +2、Developer settings + +![image-20210517142543311](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210517142546.png) + + + +3、Personal access tokens + +![image-20210517142607486](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210517142616.png) + +4、Generate new token + +![image-20210517142727516](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210517142730.png) + +**创建成功后,会生成一串token,这串token之后不会再显示,请认真保存** + + + +### 配置秘钥 + +仓库Setting -> secrets -> New repository secret + +![image-20210517152652936](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210517152655.png) + + + +Name必须填 `ACCESS_TOKEN` + +Value填写上一步生成的Token + +![image-20210517152823937](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210517152825.png) + +至此已全部配置好 + +每当我们 push 到主分支 master 时候,github pages 会自动部署 + diff --git "a/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/11.\346\240\207\351\242\230\347\233\256\345\275\225\347\224\237\346\210\220\345\217\212\345\221\275\345\220\215\345\273\272\350\256\256.md" "b/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/11.\346\240\207\351\242\230\347\233\256\345\275\225\347\224\237\346\210\220\345\217\212\345\221\275\345\220\215\345\273\272\350\256\256.md" new file mode 100644 index 00000000..e18bf890 --- /dev/null +++ "b/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/11.\346\240\207\351\242\230\347\233\256\345\275\225\347\224\237\346\210\220\345\217\212\345\221\275\345\220\215\345\273\272\350\256\256.md" @@ -0,0 +1,165 @@ +--- +title: 标题目录生成及命名建议 +date: 2021-05-16 23:36:45 +permalink: /about/blog/title/2 +--- + + + + + + + +- [标题目录生成](#%E6%A0%87%E9%A2%98%E7%9B%AE%E5%BD%95%E7%94%9F%E6%88%90) + - [typora](#typora) + - [vuepress](#vuepress) + - [DoCToc](#doctoc) +- [标题建议](#%E6%A0%87%E9%A2%98%E5%BB%BA%E8%AE%AE) + + + +## 标题目录生成 + +推荐使用DocToc + +### typora + +输入 + +```markdown +[TOC] +``` + +生成所有标题 + +但不能在 github vuepress 中正常显示,可在 csdn 中使用 + + + +### vuepress + +```markdown +[[TOC]] +``` + +只生成二级、三级标题 + +只在 vuepress 中显示 + + + +### DoCToc + +**建议使用 DocToc** + +[DocToc](https://github.com/thlorenz/doctoc):为本地git仓库内的markdown文件生成目录。链接与github或其他站点生成的锚点兼容。 + + + +**全局安装** + +```sh +npm install -g doctoc +``` + + + +**使用方法:** + +官方文档:[https://www.npmjs.com/package/doctoc](https://www.npmjs.com/package/doctoc) + +DocToc 默认在顶头生成目录,但在 vuepress 中顶头写的是配置 + +所以需要自定义目录位置 + +```markdown + + +``` + +例如: + +```markdown +// my_new_post.md +Here we are, introducing the post. It's going to be great! +But first: a TOC for easy reference. + + + + +# Section One + +Here we'll discuss... + +``` + + + +**生成目录** + +生成一个文件 `README.md` + +```sh +doctoc README.md --github +``` + +**更新目录** + +更新一个文件 `README.md` + +```sh +doctoc README.md --github -u +``` + +**目录下所有文件一键生成** + +在项目根目录中 + +```sh +doctoc . --github -u +``` + +此命令只会在有标记的md文件生成、更新目录 + +若你想每个文件都生成,需要自己在每个文件中加上标记 + +## 标题建议 + +为了链接适配 github 及 vuepress 标题锚点兼容 + +**建议** + +- 不要使用小写数字+空格,可用大写数字+空格 + +- 不要使用符号 + + 可以用空格,且只能一个空格 + +- 三级标题及更小标题不可添加符号及空格 + +正确: + +```markdown +## 一 java简介 +## 二 Java的特性和优势 +## Java三大版本 +``` + +错误 + +```markdown +//使用二个空格 +## 一 java简介 +//使用符号 +## 2、java简介 + +//三级及以下标题不可加符号和空格 +### 三 Java三大版本 +``` + + + + + +两者生成的锚点链接差异:访问下文 + diff --git "a/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/12.\346\240\207\351\242\230\351\224\232\347\202\271\346\257\224\350\276\203.md" "b/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/12.\346\240\207\351\242\230\351\224\232\347\202\271\346\257\224\350\276\203.md" new file mode 100644 index 00000000..cf0ea494 --- /dev/null +++ "b/docs/10.\345\205\263\344\272\216/02.\345\205\263\344\272\216 - \346\234\254\347\253\231/12.\346\240\207\351\242\230\351\224\232\347\202\271\346\257\224\350\276\203.md" @@ -0,0 +1,79 @@ +--- +title: 标题锚点比较 +date: 2021-05-16 22:54:18 +permalink: /about/blog/title/3 +--- + +--- + +vuepress生成的目录 + +[[TOC]] + +--- + +点击两款标题跳转的锚点,自行判断二者的区别 + +----------------------------- + +doctoc生成的目录 + + + + + +- [一 二级目录2空格](#%E4%B8%80--%E4%BA%8C%E7%BA%A7%E7%9B%AE%E5%BD%952%E7%A9%BA%E6%A0%BC) + - [3 相同的三级目录](#3--%E7%9B%B8%E5%90%8C%E7%9A%84%E4%B8%89%E7%BA%A7%E7%9B%AE%E5%BD%95) + - [3 相同的三级目录](#3--%E7%9B%B8%E5%90%8C%E7%9A%84%E4%B8%89%E7%BA%A7%E7%9B%AE%E5%BD%95-1) + - [不带符号和空格的四级目录](#%E4%B8%8D%E5%B8%A6%E7%AC%A6%E5%8F%B7%E5%92%8C%E7%A9%BA%E6%A0%BC%E7%9A%84%E5%9B%9B%E7%BA%A7%E7%9B%AE%E5%BD%95) +- [二 二级目录1空格](#%E4%BA%8C-%E4%BA%8C%E7%BA%A7%E7%9B%AE%E5%BD%951%E7%A9%BA%E6%A0%BC) +- [二 二级目录(带括号)](#%E4%BA%8C-%E4%BA%8C%E7%BA%A7%E7%9B%AE%E5%BD%95%E5%B8%A6%E6%8B%AC%E5%8F%B7) + - [3 三级目录](#3-%E4%B8%89%E7%BA%A7%E7%9B%AE%E5%BD%95) + - [4 四级目录](#4-%E5%9B%9B%E7%BA%A7%E7%9B%AE%E5%BD%95) +- [2、带数字和符号的二级目录](#2%E5%B8%A6%E6%95%B0%E5%AD%97%E5%92%8C%E7%AC%A6%E5%8F%B7%E7%9A%84%E4%BA%8C%E7%BA%A7%E7%9B%AE%E5%BD%95) + - [3、三级目录](#3%E4%B8%89%E7%BA%A7%E7%9B%AE%E5%BD%95) + - [4、四级目录](#4%E5%9B%9B%E7%BA%A7%E7%9B%AE%E5%BD%95) + + + + +----------------------------- + + + +## 一 二级目录2空格 + +### 3 相同的三级目录 + +### 3 相同的三级目录 + +#### 不带符号和空格的四级目录 + + + +## 二 二级目录1空格 + +## 二 二级目录(带括号) + + + +### 3 三级目录 + + + +#### 4 四级目录 + + + +## 2、带数字和符号的二级目录 + + + +### 3、三级目录 + + + + + +#### 4、四级目录 + diff --git "a/docs/50.C\350\257\255\350\250\200/01.C\350\257\255\350\250\200\345\217\221\345\261\225\345\216\206\347\250\213.md" "b/docs/50.C\350\257\255\350\250\200/01.C\350\257\255\350\250\200\345\217\221\345\261\225\345\216\206\347\250\213.md" new file mode 100644 index 00000000..6a7f849c --- /dev/null +++ "b/docs/50.C\350\257\255\350\250\200/01.C\350\257\255\350\250\200\345\217\221\345\261\225\345\216\206\347\250\213.md" @@ -0,0 +1,43 @@ +--- +title: C语言发展历程 +date: 2021-05-11 14:30:00 +permalink: /c/note0/ +author: + name: eric + href: https://wfmiss.cn +--- +# C语言发展历程 +## 历史介绍及环境搭建 +- C语⾔是从B语⾔发展⽽来的,B语⾔是从`BCPL`发展⽽来的,`BCPL`是从 `FORTRAN`发展⽽来的。 +- `BCPL`和B都⽀持指针间接⽅式,所以C也⽀持了。 +- C语⾔还受到了`PL/1`的影响,还和`PDP-11`的机器语⾔有很⼤的关系。 +- 1973年3⽉,第三版的`Unix`上出现了C语⾔的编译器。 +- 1973年11⽉,第四版的`Unix(System Four)`发布了,这个版本是完全用C语言重新编写的。 + +## C的发展与版本-K&R +- 经典 C ----> ⼜被叫做 `“K&R the C”` +- The C Programming Language, by Brian Kernighan and Dennis Ritchie, 2nd Edition, Prentice Hall + +## C的发展与版本-标准 +- 1989年ANSI发布了⼀个标准——`ANSI C` +- 1990年ISO接受了ANSI的标准——C89 +- C的标准在1995年和1999年两次更新——C95和C99 +- **所有的当代编译器都⽀持C99了** + +## C语⾔⽤在哪⾥? + +- 操作系统 +- 嵌⼊式系统 +- 驱动程序 +- 底层驱动 +- 图形引擎、图像处理、声⾳效果 + +## C语言编译软件(IDE) +- Dev C++(4.9 for Win7, 5.0 for Win8) +- MS Visual Studio Express(Windows) +- Xcode(Mac OS X) +- Eclipse-CDT +- Geany(和MinGW⼀起) +- Sublime(和MinGW⼀起) +- vim/emacs(和MinGW⼀起) + diff --git "a/docs/50.C\350\257\255\350\250\200/02.\347\254\254\344\270\200\347\253\240\345\237\272\347\241\200\350\257\255\346\263\225.md" "b/docs/50.C\350\257\255\350\250\200/02.\347\254\254\344\270\200\347\253\240\345\237\272\347\241\200\350\257\255\346\263\225.md" new file mode 100644 index 00000000..ac208cd7 --- /dev/null +++ "b/docs/50.C\350\257\255\350\250\200/02.\347\254\254\344\270\200\347\253\240\345\237\272\347\241\200\350\257\255\346\263\225.md" @@ -0,0 +1,206 @@ +--- +title: 第一章 基础语法 +date: 2021-05-11 14:30:00 +permalink: /c/note1/ +author: + name: eric + href: https://wfmiss.cn +--- +# 第一章 基础语法 + +**注意事项:编写代码注意:代码里的符号一定要是英文状态下的标点符号!!!,不要用中文!!!** + +## 1、输出函数 +`printf("Hello World!\n");` +`""`⾥⾯的内容叫做`“字符串”`,`printf`会把其中的内容原封不动地输出` \n `表⽰需要在输出的结果后⾯换⼀⾏。 + +补充: +- 如果你在使⽤Dev C++ 4.9.9.2 +`system("pause");` +- 让程序运⾏完成后,窗⼝还能留下 +- 不是Dev C++ 4.9.9.2就不需要这个了 + +## 2、运算符 + +**算数运算符** +|C符号|意义| +|:---:|:---:| +|+|加| +|-|减| +|`*`|乘| +|/|除| +|%|取余| +|()|括号| +|++|自增| +|--|自减| + +**关系运算符** +|C符号|意义| +|:---:|:---:| +|>|大于| +|<|小于| +|==|等于| +|>=|大于等于| +|<=|小于等于| +|!=|不等于| + +**逻辑运算符** +|C符号|意义| +|:---:|:---:| +|!|非| +|&&|与| +|`||`|或| + +位运算符:【>>,<<,~,|,^,&】 +赋值运算符:【`=`】 +条件运算符(三元运算符):【`常量表达式?返回值1:返回值2;`】 +逗号运算符:【`,`】 +指针运算符:【`*`,&】 +求字节数运算符:【`sizeof`】 + +## 3、变量定义 +变量定义的⼀般形式就是:`<类型名称> <变量名称>;` +例如:`int a;` + +## 4、变量的名字 +- 变量需要⼀个名字,变量的名字是⼀种“标识符”,意思是它是⽤来识别这个和那个的不同的名字。 + +- 标识符有标识符的构造规则。基本的原则是:标识符只能由字⺟、数字和下划线组成,数字不可以出现在第⼀个位置上,C语⾔的关键字(有的地⽅叫它们保留字)不可以⽤做标识符。 + **C语⾔的保留字:** + +```txt + auto、break、case、char、const 、continue、default、do、double 、else、enum、extern、float、for、goto、if、int、long、register、return、short、signed、sizeof、static、 struct、switch、typedef、union、unsigned、void、volatile、while、inline、restrict +``` + + **赋值和初始化** + `int price = 0;` + 这⼀⾏,定义了⼀个变量。变量的名字是`price`,类型是`int`,初始值是`0`。 + 这⾥的`“=”`是⼀个赋值运算符,表⽰将`“=”`右边的值赋给左边的变量。 + +## 5、变量初始化 +如果没有进行初始化变量,直接使用,运算出来的结果会是一个很奇怪的值。 +格式为:`<类型名称> <变量名称> = <初始值>;` +> 当赋值发⽣在定义变量的时候,就像程序1中的第7⾏那样,就是变量的初始化。虽然C语⾔并没有强制要求所有的变量都在定义的地⽅做初始化,但是所有的变量在第⼀次被使⽤(出现在赋值运算符的右边)之前被应该赋值⼀次。 + +## 6、变量类型 +C是⼀种有类型的语⾔,所有的变量在使⽤之前必须定义或声明,所有的变量必须具有确定的数据类型。数据类型表⽰在变量中可以存放什么样的数据,变量中只能存放指定类型的数据,程序运⾏过程中也不能改变变量的类型。 + +**六种基本数据类型。** + +|变量类型|说明|字节大小| +|:---:|:---:|:---:| +|char|字符型类型|1| +|short|短整型类型|2| +|int|整型类型|4| +|long|长整型类型|4or8| +|float|单精度浮点类型|4| +|double|双精度浮点类型|8| + +- `signed`:有符号,可省略 +- `unsigned`:无符号 + +## 7、常量 + +C99允许使用**常变量**,方法是在定义变量时,前面加一个关键字`const`。 +符号常量`#define`:用法`#define 常量名 值 `,注意行末没有分号。 + +## 8、转义字符 +转义字符及其作用 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210510175613.png) + +## 9、输入函数 +`scanf(“格式控制字符串”, 地址表列);` +格式控制字符串的作用与printf函数相同,但不能显示非格式字符串,也就是不能显示提示字符串。地址表列中给出各变量的地址。地址是由地址运算符“&”后跟变量名组成的。 +例如: +```C +#include +int main(void){ + int a,b,c; + printf("input a,b,c\n"); + scanf("%d%d%d",&a,&b,&c); + printf("a=%d,b=%d,c=%d",a,b,c); + return 0; +} +``` + +** 格式字符串** +格式字符串的一般形式为: +`%[*][输入数据宽度][长度]类型` +其中有方括号[]的项为任选项。各项的意义如下。 +**类型** +表示输入数据的类型,其格式符和意义如下表所示。 + +| 格式 | 字符意义 | +| :----: | :------------------------------: | +| %d | 输入十进制整数 | +| %o | 输入八进制整数 | +| %x | 输入十六进制整数 | +| %u | 输入无符号十进制整数 | +| %f或%e | 输入实型数(用小数形式或指数形式) | +| %c | 输入单个字符 | +| %s | 输入字符串 | + +**使用scanf函数还必须注意以下几点:** + +1. scanf函数中没有精度控制,如:scanf("%5.2f",&a);是非法的。不能企图用此语句输入小数为2位的实数。 +2. scanf中要求给出变量地址,如给出变量名则会出错。如 scanf("%d",a);是非法的,应改为scnaf("%d",&a);才是合法的。 +3. 在输入多个数值数据时,若格式控制串中没有非格式字符作输入数据之间的间隔则可用空格,TAB或回车作间隔。C编译在碰到空格,TAB,回车或非法数据(如对“%d”输入“12A”时,A即为非法数据)时即认为该数据结束。 +4. 在输入字符数据时,若格式控制串中无非格式字符,则认为所有输入的字符均为有效字符。 + +**printf格式附加字符** + +| 字符 | 说明 | +| :---------------: | :----------------------------------------------------: | +| l | 长整型整数,可加在格式符d、o、x、u前面 | +| m(代表一个正整数) | 数据最小宽度 | +| n(代表一个正整数) | 对实数,表示输出n位小数;对字符串,表示截取的字符个数; | +| - | 输出的数字或字符域内向左靠 | + +一般形式为:`% 附加字符 格式字符` + +**scanf格式附加字符** +| 字符 | 说明 | +| :--: | :----------------------------------------------------------: | +| l | 输入长整型数据(可用%ld,%lo,%lx,%lu)以及double型数据(用%lf或%le) | +| h | 输入短整型数据(可用%hd,%ho,%hx) | +| 域宽 | 指定输入输入数据所占宽度(列数),宽域应为正整数 | +| `*` | 本输入项在读入后不赋给相应变量 | + +## 10、运算符优先级和结合性 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210511161914.png) + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210511161913.png) + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210511161912.png) + +**上表中可以总结出如下规律:** + +1. 结合方向只有三个是从右往左,其余都是从左往右。 +2. 所有双目运算符中只有赋值运算符的结合方向是从右往左。 +3. 另外两个从右往左结合的运算符也很好记,因为它们很特殊:一个是单目运算符,一个是三目运算符。 +4. C语言中有且只有一个三目运算符。 +5. 逗号运算符的优先级最低,要记住。 +6. 此外要记住,对于优先级:算术运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符。逻辑运算符中“逻辑非 !”除外。 + +## 11、一些容易出错的优先级问题 + +上表中,优先级同为1 的几种运算符如果同时出现,那怎么确定表达式的优先级呢?这是很多初学者迷糊的地方。下表就整理了这些容易出错的情况: + +| 优先级问题 | 表达式 | 经常误认为的结果 | 实际结果 | +| ------------------------------------------------- | -------------------- | --------------------------------------------------------- | ------------------------------------------------------------ | +| `.` 的优先级高于 `*`(-> 操作符用于消除这个问题) | `*`p.f | p 所指对象的字段 f,等价于: (`*`p).f | 对 p 取 f 偏移,作为指针,然后进行解除引用操作,等价于: `*`(p.f) | +| [] 高于 `*` | int `*`ap[] | ap 是个指向 int 数组的指针,等价于: int (`*`ap)[] | ap 是个元素为 int 指针的数组,等价于: int `*`(ap []) | +| 函数 () 高于 `*` | int `*`fp() | fp 是个函数指针,所指函数返回 int,等价于: int (`*`fp)() | fp 是个函数,返回 int`*`,等价于: int`* `( fp() ) | +| == 和 != 高于位操作 | (val & mask != 0) | (val &mask) != 0 | val & (mask != 0) | +| == 和 != 高于赋值符 | c = getchar() != EOF | (c = getchar()) != EOF | c = (getchar() != EOF) | +| 算术运算符高于位移 运算符 | msb << 4 + lsb | (msb << 4) + lsb | msb << (4 + lsb) | +| 逗号运算符在所有运 算符中优先级最低 | i = 1, 2 | i = (1,2) | (i = 1), 2 | + +## 12、不同类型数据间的混合运算 +(1)+、-、`*`、/、运算符两侧中有一个为float或double型,结果都为double型数据。 +(2)如果 int型与float型数据进行运算,会先把int型和和float型数据转换为double型,然后再进行运算,结果是double型 +(3)字符(char)型数据与整形数据进行运算,就是把字符型数据的ASCLL代码与整形数据进行运算。如:12+'A'等效于12+65结果为77,字符型数据与实型数据进行运算,则会将字符型的ASCLL代码转换为double型数据然后再进行运算。 + +以上的转换都是由编译器自动完成转换的,知道其转换的原理即可,不用自己进行转换。 diff --git "a/docs/50.C\350\257\255\350\250\200/03.\347\254\254\344\272\214\347\253\240\346\216\247\345\210\266\350\257\255\345\217\245.md" "b/docs/50.C\350\257\255\350\250\200/03.\347\254\254\344\272\214\347\253\240\346\216\247\345\210\266\350\257\255\345\217\245.md" new file mode 100644 index 00000000..90cd89ff --- /dev/null +++ "b/docs/50.C\350\257\255\350\250\200/03.\347\254\254\344\272\214\347\253\240\346\216\247\345\210\266\350\257\255\345\217\245.md" @@ -0,0 +1,53 @@ +--- +title: 第二章 控制语句 +date: 2021-05-11 14:30:00 +permalink: /c/note2/ +author: + name: eric + href: https://wfmiss.cn +--- +# 第二章 控制语句 +- if…else… 条件语句 +- for()… 循环语句 +- whille()… 循环语句 +- do…whille() 循环语句 +- continue 结束本次循环 +- break 终止执行switch或循环语句 +- switch 多分支语句 +- return 从函数返回语句 +- goto 转向语句,在结构化程序中基本不用goto语句 + +## 1. 字符输入输出函数 + +`putchar`输出一个字符 + +`getchar`输入一个字符 + +## 2. 选择结构嵌套 + +```c +if(){ + if()语句; + else()语句; +} +else{ + if()语句; + else()语句; +} +``` + +## 3. switch多分支 + +```c +switch(表达式) +{ + case 常量1:语句1;break; + case 常量2:语句2;break; + …… + case 常量n:语句n;break; + default :语句 n+1;break; +} +``` + +由于用法基本一致,其余不做详细介绍。请参考:《C语言程序设计(第五版)》——谭浩强 【第五章-循环结构 -110页】 + diff --git "a/docs/50.C\350\257\255\350\250\200/04.\347\254\254\344\270\211\347\253\240\346\225\260\347\273\204.md" "b/docs/50.C\350\257\255\350\250\200/04.\347\254\254\344\270\211\347\253\240\346\225\260\347\273\204.md" new file mode 100644 index 00000000..04fffbd3 --- /dev/null +++ "b/docs/50.C\350\257\255\350\250\200/04.\347\254\254\344\270\211\347\253\240\346\225\260\347\273\204.md" @@ -0,0 +1,309 @@ +--- +title: 第三章 数组 +date: 2021-05-11 14:30:00 +permalink: /c/note3/ +author: + name: eric + href: https://wfmiss.cn +--- +# 第三章 数组【一维数组、多维数组】 + +## 1. 一维数组 + +一维数组的定义方式为: +`类型说明符 数组名 [常量表达式];` +其中,类型说明符是任一种基本数据类型或构造数据类型。数组名是用户定义的数组标识符。方括号中的常量表达式表示数据元素的个数,也称为数组的长度。例如: + +```c +int a[10]; /* 说明整型数组a,有10个元素 */ +float b[10], c[20]; /* 说明实型数组b,有10个元素,实型数组c,有20个元素 */ +char ch[20]; /* 说明字符数组ch,有20个元素 */ +``` +**对于数组类型说明应注意以下几点:** +1) 数组的类型实际上是指数组元素的取值类型。对于同一个数组,其所有元素的数据类型都是相同的。 +2) 数组名的书写规则应符合标识符的书写规定。 +3) 数组名不能与其它变量名相同。例如: +```c +int a; +float a[10]; +``` +是错误的。 + +4) 方括号中常量表达式表示数组元素的个数,如a[5]表示数组a有5个元素。但是其下标从0开始计算。因此5个元素分别为a[0], a[1], a[2], a[3], a[4]。 + +5) 不能在方括号中用变量来表示元素的个数,但是可以是符号常数或常量表达式。例如: + +```c +#define FD 5 // 宏定义,FD为常量(值不可改变) +int a[3+2],b[7+FD]; +``` +是合法的。但是下述说明方式是错误的。 +```c +int n=5; +int a[n]; +``` +6) 允许在同一个类型说明中,说明多个数组和多个变量。例如: +```c +int a,b,c,d,k1[10],k2[20]; +``` + +**一维数组元素的引用** +数组元素是组成数组的基本单元。数组元素也是一种变量, 其标识方法为数组名后跟一个下标。下标表示了元素在数组中的顺序号。数组元素的一般形式为: +`数组名[下标]` +其中下标只能为整型常量或整型表达式。如为小数时,C编译将自动取整。例如: + a[5] + a[i+j] + a[i++] +都是合法的数组元素。 + +数组元素通常也称为下标变量。必须先定义数组,才能使用下标变量。在C语言中只能逐个地使用下标变量,而不能一次引用整个数组。 + +**一维数组的初始化** + +一维数组的初始化可以使用以下方法实现: +1) 定义数组时给所有元素赋初值,这叫“完全初始化”。例如: + +``` +int a[5] = {1, 2, 3, 4, 5}; +``` + +通过将数组元素的初值依次放在一对花括号中,如此初始化之后,a[0]=1;a[1]=2;a[2]=3;a[3]=4;a[4]=5,即从左到右依次赋给每个元素。需要注意的是,初始化时各元素间是用逗号隔开的,不是用分号。 + +2) 可以只给一部分元素赋值,这叫“不完全初始化”。例如: + +``` +int a[5] = {1, 2}; +``` + +定义的数组 a 有 5 个元素,但花括号内只提供两个初值,这表示只给前面两个元素 a[0]、a[1] 初始化,而后面三个元素都没有被初始化。不完全初始化时,没有被初始化的元素自动为 0。 + +需要注意的是,“不完全初始化”和“完全不初始化”不一样。如果“完全不初始化”,即只定义“int a[5];”而不初始化,那么各个元素的值就不是0了,所有元素都是垃圾值。 + +你也不能写成“int a[5]={};”。如果大括号中什么都不写,那就是极其严重的语法错误。大括号中最少要写一个数。比如“int a[5]={0};”,这时就是给数组“清零”,此时数组中每个元素都是零。此外,如果定义的数组的长度比花括号中所提供的初值的个数少,也是语法错误,如“a[2]={1,2,3,4,5};”。 + +3) 如果定义数组时就给数组中所有元素赋初值,那么就可以不指定数组的长度,因为此时元素的个数已经确定了。编程时我们经常都会使用这种写法,因为方便,既不会出问题,也不用自己计算有几个元素,系统会自动分配空间。例如: + +``` +int a[5] = {1, 2, 3, 4, 5}; +``` + +可以写成: + +``` +int a[] = {1, 2, 3, 4, 5}; +``` + +第二种写法的花括号中有 5 个数,所以系统会自动定义数组 a 的长度为 5。但是要注意,只有在定义数组时就初始化才可以这样写。如果定义数组时不初始化,那么省略数组长度就是语法错误。比如: + +``` +int a[]; +``` + +那么编译时就会提示错误,编译器会提示你没有指定数组的长度。 + +## 2. 二维数组 +二维数组定义的一般形式是: +`类型说明符 数组名[常量表达式1][常量表达式2]` +其中常量表达式1表示第一维下标的长度,常量表达式2 表示第二维下标的长度。例如: +```c +int a[3][4]; +``` +说明了一个三行四列的数组,数组名为a,其下标变量的类型为整型。 +该数组的下标变量共有3×4个,即: +``` + a[0][0], a[0][1], a[0][2], a[0][3] + a[1][0], a[1][1], a[1][2], a[1][3] + a[2][0], a[2][1], a[2][2], a[2][3] +``` +二维数组在概念上是二维的,即是说其下标在两个方向上变化,下标变量在数组中的位置也处于一个平面之中,而不是象一维数组只是一个向量。但是,实际的硬件存储器却是连续编址的,也就是说存储器单元是按一维线性排列的。如何在一维存储器中存放二维数组,可有两种方式:一种是按行排列, 即放完一行之后顺次放入第二行。另一种是按列排列, 即放完一列之后再顺次放入第二列。 + +在C语言中,二维数组是按行排列的。即,先存放a[0]行,再存放a[1]行,最后存放a[2]行。每行中有四个元素也是依次存放。由于数组a说明为int类型,该类型占两个字节的内存空间,所以每个元素均占有两个字节。 + +**二维数组元素的引用** +二维数组的元素也称为双下标变量,其表示的形式为: +`数组名[下标][下标]` +其中下标应为整型常量或整型表达式。例如: + `a[3][4]` +表示a数组三行四列的元素。 + +下标变量和数组说明在形式中有些相似,但这两者具有完全不同的含义。数组说明的方括号中给出的是某一维的长度,即可取下标的最大值;而数组元素中的下标是该元素在数组中的位置标识。前者只能是常量,后者可以是常量,变量或表达式。 + +**二维数组元素的初始化** +二维数组初始化也是在类型说明时给各下标变量赋以初值。二维数组可按行分段赋值,也可按行连续赋值。 +**对于二维数组初始化赋值还有以下说明:** +1) 可以只对部分元素赋初值,未赋初值的元素自动取0值。例如: + +``` +int a[3][3]={{1},{2},{3}}; +``` +是对每一行的第一列元素赋值,未赋值的元素取0值。 赋值后各元素的值为: + 1 0 0 + 2 0 0 + 3 0 0 +``` +int a [3][3]={{0,1},{0,0,2},{3}}; +``` +赋值后的元素值为: + 0 1 0 + 0 0 2 + 3 0 0 + +2) 如对全部元素赋初值,则第一维的长度可以不给出。例如: +``` +int a[3][3]={1,2,3,4,5,6,7,8,9}; +``` +可以写为: +``` +int a[][3]={1,2,3,4,5,6,7,8,9}; +``` +3) 数组是一种构造类型的数据。二维数组可以看作是由一维数组的嵌套而构成的。设一维数组的每个元素都又是一个数组,就组成了二维数组。当然,前提是各元素类型必须相同。根据这样的分析,一个二维数组也可以分解为多个一维数组。C语言允许这种分解。 +如二维数组`a[3][4]`,可分解为三个一维数组,其数组名分别为: + a[0] + a[1] + a[2] +对这三个一维数组不需另作说明即可使用。这三个一维数组都有4个元素,例如:一维数组`a[0]`的元素为`a[0][0],a[0][1],a[0][2],a[0][3]`。必须强调的是,`a[0],a[1],a[2]`不能当作下标变量使用,它们是数组名,不是一个单纯的下标变量。 + +## 3. 字符数组 +**1、字符数组的定义** +`char word[] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’,‘!’,’\0’};` +以0(整数0)结尾的⼀串字符 +0或`’\0’`是⼀样的,但是和’0’不同 +0标志字符串的结束,但它不是字符串的⼀部分 +计算字符串⻓度的时候不包含这个0 +字符串以数组的形式存在,以数组或指针的形式访问 +更多的是以指针的形式 +**2、字符数组与字符串** +• `"Hello" ` +• `"Hello"` 会被编译器变成⼀个字符数组放在某处,这 +个数组的⻓度是6,结尾还有表⽰结束的0 +• 两个相邻的字符串常量会被⾃动连接起来 +• ⾏末的\表⽰下⼀⾏还是这个字符串常量 + +******** +• C语⾔的字符串是以字符数组的形态存在的 +• 不能⽤运算符对字符串做运算 +• 通过数组的⽅式可以遍历字符串 +• 唯⼀特殊的地⽅是字符串字⾯量可以⽤来初始化字符 +数组 +• 以及标准库提供了⼀系列字符串函数 +******** +char`*`s = "Hello, world!"; +• s 是⼀个指针,初始化为指向⼀个字符串常量 +• 由于这个常量所在的地⽅,所以实际上s是 const +char`*` s ,但是由于历史的原因,编译器接受不带 +const的写法 +• 但是试图对s所指的字符串做写⼊会导致严重的后果 +• 如果需要修改字符串,应该⽤数组:`char s[] = "Hello, world!";` + +**指针还是数组?** +• `char *str = "Hello";` +• `char word[] = "Hello";` +• 数组:这个字符串在这⾥ +• 作为本地变量空间⾃动被回收 +• 指针:这个字符串不知道在哪⾥ +• 处理参数 +• 动态分配空间 + +- 如果要构造⼀个字符串—>数组 ++ 如果要处理⼀个字符串—>指针 + +**3、字符串的表示形式** +在C语言中,可以用两种方法表示和存放字符串: +(1)用字符数组存放一个字符串 +`char str[]="I love China";` +(2)用字符指针指向一个字符串 +`char* str="I love China";` +对于第二种表示方法,有人认为str是一个字符串变量,以为定义时把字符串常量"I love China"直接赋给该字符串变量,这是不对的。 +C语言对字符串常量是按字符数组处理的,在内存中开辟了一个字符数组用来存放字符串常量,程序在定义字符串指针变量str时只是把字符串首地址(即存放字符串的字符数组的首地址)赋给str。 + +**两种表示方式的字符串输出都用** +`printf("%s\n",str);` +%s表示输出一个字符串,给出字符指针变量名str(对于第一种表示方法,字符数组名即是字符数组的首地址,与第二种中的指针意义是一致的),则系统先输出它所指向的一个字符数据,然后自动使str自动加1,使之指向下一个字符...,如此,直到遇到字符串结束标识符 `" \0 "`。 + +• 字符串可以表达为char*的形式 • char*不⼀定是字符串 • 本意是指向字符的指针,可能指向的是字符 的数组(就像int*⼀样) • 只有它所指的字符数组有结尾的0,才能说它 所指的是字符串 + + + +**4、对使用字符指针变量和字符数组两种方法表示字符串的讨论** +虽然用字符数组和字符指针变量都能实现字符串的存储和运算,但它们二者之间是有区别的,不应混为一谈。 +4.1、字符数组由若干个元素组成,每个元素放一个字符;而字符指针变量中存放的是地址(字符串/字符数组的首地址),绝不是将字符串放到字符指针变量中(是字符串首地址) +4.2、**赋值方式:** 对字符数组只能对各个元素赋值,不能用以下方法对字符数组赋值 + +``` +char str[14]; +str="I love China"; +//(但在字符数组**初始化**时可以,即char str[14]="I love China";) +``` +而对字符指针变量,采用下面方法赋值: +``` +char* a; +a="I love China"; +``` +或者是`char* a="I love China";`都可以 + +4.3、对字符指针变量赋初值(**初始化**): +`char* a="I love China";` +等价于: +``` +char* a; +a="I love China"; +``` +而对于字符数组的初始化 +`char str[14]="I love China";` +不能等价于: +``` +char str[14]; +str="I love China"; (这种不是初始化,而是赋值,而对数组这样赋值是不对的) +``` +4.4、如果定义了一个字符数组,那么它有确定的内存地址;而定义一个字符指针变量时,它并未指向个确定的字符数据,并且可以多次赋值。 + +*********** + +**5、字符串处理函数** + +**注意:**在使用字符串处理函数函数时应当在程序文件的开头用`#include `。 + +- puts函数 + - 输出字符串:`puts(字符数组)` + +- gets函数 + - 输入字符串:`gets(字符数组)` + +- strcat函数 + - 字符串连接函数:`strcat(字符串数组1,字符串数组2)` + +- strcpy函数 + + - 字符串复制函数:`strcpy(字符串数组1,字符串数组2)` + +- strncpy函数 + + - 字符串复制函数:`strncpy(字符串数组1,字符串数组2,n)`,`n`为常数 + - 把字符串数组2中前面`n`个字符复制到字符串数组1中 + +- strcmp函数 + + - 字符串比较函数:`strcmp(字符串数组1,字符串数组2)` + + - 比较规则:将两个字符串自左向右逐个字符相比(按ASCII码值大小比较),直到出现不同的字符或遇到`‘\0’`为止。 + + >1.如果字符串数组1与字符串数组2相同,则返回函数值为0。 + > + >2.如果字符串数组1>字符串数组2相同,则返回函数值为一个正整数。 + > + >3.如果字符串数组1<字符串数组2相同,则返回函数值为一个负整数。 + +- strlen函数 + + - 测字符串长度:`strlen(字符串数组)` + - 函数的值为字符串中的实际长度(不包括`'\0'`在内) + +- strlwr函数 + + - 转换为小写:`strlwr(字符串数组)` + +- strupr函数 + + - 转换为小写:`strupr(字符串数组)` + +****** diff --git "a/docs/50.C\350\257\255\350\250\200/05.\347\254\254\345\233\233\347\253\240\345\207\275\346\225\260\345\256\236\347\216\260\346\250\241\345\235\227\345\214\226\350\256\276\350\256\241.md" "b/docs/50.C\350\257\255\350\250\200/05.\347\254\254\345\233\233\347\253\240\345\207\275\346\225\260\345\256\236\347\216\260\346\250\241\345\235\227\345\214\226\350\256\276\350\256\241.md" new file mode 100644 index 00000000..8d0abd6b --- /dev/null +++ "b/docs/50.C\350\257\255\350\250\200/05.\347\254\254\345\233\233\347\253\240\345\207\275\346\225\260\345\256\236\347\216\260\346\250\241\345\235\227\345\214\226\350\256\276\350\256\241.md" @@ -0,0 +1,381 @@ +--- +title: 第四章 函数实现模块化设计 +date: 2021-05-11 14:30:00 +permalink: /c/note4/ +author: + name: eric + href: https://wfmiss.cn +--- +# 第四章 函数实现模块化设计 + +## 1. 定义函数的方法 + +函数体包括**声明部分**和**语句部分** + +- 定义无参函数 + +一般形式为: + +``` +类型名 函数名(){ + 函数体 +} +或 +类型名 函数名(void){ + 函数体 +} +``` + +- 定义有参函数 + +一般形式为: + +``` +类型名 函数名(形式参数列表){ + 函数体 +} +``` + +- 定义空参函数 + - 函数体是空的。调用此函数时,什么工作也不做,没有任何实际作用。 + +一般形式为: + +``` +类型名 函数名() +{} +``` +## 2. 调用函数 + +**调用函数的形式** + +一般的调用形式为: + +```C +函数名(实参表列); +``` + +函数调用语句:把函数调用单独作为一个语句。 + +函数表达式:函数出现在另一个表达式中。 + +函数参数:函数调用作为另外一个函数调用时的参数。 + +**函数作为参数时的数据传递** +*【函数形式参数和实际参数】* + +函数的参数分为两种,分别是形式参数与实际参数。 + +①形式参数: + +在定义函数时函数名后面括号中的变量名称称为形式参数(简称形参),即形参出现在函数定义中。形参变量只有在被调用时才会为其分配内训单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只在函数内部有效,只有当函数被调用时,系统才为形参分配存储单元,并完成实参与形参的数据传递。在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。 + +②实际参数: + +主调函数中调用一个函数时,函数名后面括号中的参数称为实际参数(简称实参),即实参出现在主调函数中。 + +实参可以是常量,变量,表达式,函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传递给形参。因此应预先用赋值,输入等办法使实参获得确定值。 + +说明:在被定义的函数中,必须指定形参的类型。实参与形参的类型应相同或赋值兼容。实参和形参在数量上,类型上,顺序上应该严格一致,否则会发生类型不匹配的错误。 + +## 3. 函数的返回值 + +1. 函数的返回值是通过函数中的return语句获得的。 + + 【return语句将被调用函数中的一个确定值带回到主函数中去。】 + +2. 函数值的类型。既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型。 + + 【注意:在定义函数时要指定函数的类型。】 + +3. 在定义函数时指定的函数类型一般应该和return语句中的表达式类型一致。 + + 【如果函数的类型和return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。即**函数类型决定返回值的类型。**】 + +4. 对于不带回值的函数,应当用定义函数为**void类型(或称“空类型”)** + +## 4. 对被调用函数的声明和函数原型 + +在一个函数中调用另一个函数(即被调用函数)需要具备如下条件: + +- 首先被调用的函数必须是已经定义的函数(函数库或用户自定义的函数)。 + +- 如果使用函数,应该在本文件头用`#include`指令将调用有关库函数时所需用的到的信息“包含”到文件中来。 + +- 如果使用用户自定义的函数,而该函数的位置在调用它的函数(即主函数)的后面(在同一个文件中),应该在主函数中对被调用的函数作**声明(delcaration)**。声明的作用是把函数名、函数参数的个数和参数类型等信息通知编译系统,以便在遇到函数调用时,编译系统能正确识别到函数并检查调用是否合法。 + +函数声明的一般形式有两种: + +方式一: + +```txt +函数类型 函数名(参数类型1 参数名1,参数类型2 参数名2,…,参数类型n 参数名n); +``` + +方式二: + +```txt +函数类型 函数名(参数类型1,参数类型2,…,参数类型n); +``` + +**注意:** + +函数的 “定义“ 和 ”声明“ 不是同一回事。 + +- 函数的定义是指对函数功能的确立,包括指定函数名、函数值类型、形参及其类型以及函数体等,它是一个完整的、独立的函数单位。 +- 函数声明的作用则是把函数的名字、函数类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查,它不包含函数体。 + +## 5. 数组作为函数参数 + +**数组元素作函数实参** + +数组元素可以用作函数实参,但是不能用作形参。因为形参是在函数被调用时临时分配的存储单元,不可能为一个数组元素单独分配存储单元(数组是一个整体,在内存中占连续的一段存储单元)。在用数组元素作函数参数实参时,把实参的值传给形参,是 ”值传递“ 方式。数据传递方向是从实参传到形参,单向传递。 + +**一维数组名作函数参数** + +除了可以用数组元素作为函数参数外,还可以用数组名作函数参数(包括实参和形参)。 + +注意:用数组元素作实参时,向形参变量传递的是数组元素的值,而用数组名作函数函数参数时,向形参(数组名或指针变量)传递的是地址值。 + +**多维数组名作函数参数** + +由于用法基本一致,其余不做详细介绍。 + +请参考:《C语言程序设计(第五版)》——谭浩强 【第七章- 用函数实现模块化程序设计 -167页】 + +## 6. 局部变量和全局变量 + +变量按存储区域分:全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。 + +变量按作用域分: +- 全局变量:在整个工程文件内都有效;“在函数外定义的变量”,即从定义变量的位置到本源文件结束都有效。由于同一文件中的所有函数都能引用全局变量的值,因此如果在一个函数中改变了全局变量的值, 就能影响到其他函数中全局变量的值。 + +- 静态全局变量:只在定义它的文件内有效,效果和全局变量一样,不过就在本文件内部; + +- 静态局部变量:只在定义它的函数内有效,只是程序仅分配一次内存,函数返回后,该变量不会消失;静态局部变量的生存期虽然为整个工程,但是其作用仍与局部变量相同,即只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。      + +- 局部变量:在定义它的函数内有效,但是函数返回后失效。“在函数内定义的变量”,即在一个函数内部定义的变量,只在本函数范围内有效。 + +注意:全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知 + +静态局部变量与全局变量最明显的区别就在于:全局变量在其定义后所有函数都能用,但是静态局部变量只能在一个函数里面用。 + +形参变量 : 只在被调用期间才分配内存单元,调用结束立即释放。 + +## 7.变量的存储方式和生存期 + +**变量的存储方式有两种:** + +- 静态存储方式:是指程序在运行期间由系统分配固定的存储空间的方式。 + +- 动态存储方式:是指在程序运行期间根据需要进行动态的分配存储空间的方式。 + +供用户使用的存储空间可分为3个部分:程序区,静态存储区,动态存储区。 + +全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。 + +**动态存储区中存放以下数据:** + +1. 函数形式参数。在调用函数时给形参分配存储空间。 +2. 函数中定义的没有用static关键字声明的变量,即自动变量。 +3. 函数调用时的现场保护和返回地址等。 + +每一个变量和函数都有两个属性:数据类型和数据的存储类别。【存储类别指的是数据在内存中存储的方式:静态存储和动态存储】 + +在定义和声明变量和函数时,一般应该同时指定其数据类型和存储类别,也可以采用默认方式指定(即如果用户不指定,系统会隐含地指定为某一种存储类别)。 + +**存储类别包括4种:**自动的(auto)、静态的(static)、寄存器的(register)、外部的(extern)。 + +- 自动变量(auto变量) + + - 函数中的局部变量,如果不专门声明为static(静态)存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。函数的形参和在函数定义的局部变量(包括在复合语句中定义的局部变量),都属于此类。在调用该函数时,系统会给这些变量分配存储空间,在函数调用调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。 + + - 关键字auto可以省略不写,不写auto则隐含的指定为 “自动存储类别” ,它属于动态存储的方式。程序中大多数变量都属于自动变量。 + +- 静态局部变量(static局部变量) + + - 静态局部变量属于静态存储类别,在静态存储区域内分配存储单元。在整个程序运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,分配在动态存储区空间而不再静态存储区空间,函数调用结束后即释放。 + - 对静态局部变量是在编译时赋初值的,即只赋一次初值,在程序运行时它已有初值。以后每次调用函数函数时不再重新赋初值而只是保留上次函数调用结束时的值。而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行的,每调用一次函数重新给一次初值,相当于执行一次赋值语句。 + - 如果在定义局部变量时不赋值的话,则对静态局部变量来说,编译时自动赋初值0(对数值变量)或空字符`'\0'`(对字符变量)。而对自动变量来说,它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的内容是不可加的。 + - 虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用他的。因为它是局部变量,只能被本函数引用,而不能被其他函数引用。 + +- 寄存器变量(register变量) + + - 寄存器变量的定义形式是: + + `register 类型标识符 变量名` + + - 寄存器是与机器硬件密切相关的,不同类型的计算机,寄存器的数目是不一样的,通常为2到3个,对于在一个函数中说明的多于2到3个的寄存器变量,C编译程序会自动地将寄存器变量变为自动变量。 + + - 由于受硬件寄存器长度的限制,所以寄存器变量只能是char、int或指针型。寄存器说明符只能用于说明函数中的变量和函数中的形参,因此不允许将外部变量或静态变量说明为"register"。 + + - register型变量常用于作为循环控制变量,这是使用它的高速特点的最佳场合。比较下面两个程序的运算速度。 + +- 注意三种局部变量的存储位置是不同的 + - 自动变量存储在动态存储区 + - 静态局部变量存储在静态存储区 + - 寄存器存储在CPU中的寄存器中 + +**全局变量的存储类别** + +- 在一个文件内扩展外部变量的作用域 + - 如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义点之前的函数不能引用该外部变量。如果由于某种考虑,在定义点之前的函数需要引用该外部变量,则应该在引用之前用关键字`extern`对该变量作**“外部变量声明”**,表示把该外部变量的作用域扩展到此位置。有了此声明,就可以从 “声明” 处起,合法地使用该外部变量。 + - 注意:提倡将外部变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个`extern`声明。 + - 用`extern`声明外部变量时,类型名也可以省写。例如:`extern int A,B,C;`——>`extern A,B,C` 。 +- 将外部变量的作用域扩展到其他文件 + - 第一种情况是在同一个源文件中使用外部变量的方法,如果有多个源文件,想在A文件中引用B文件中的已定义外部变量,该如何做? + + - 假设一个程序包含两个文件,两个文件都需要用到同一个外部变量Num,若在两个文件中各自定义一个外部变量Num,将会在进行程序的连接时出现“重复定义”的错误。 + + - 因此,正确的做法是:在任一个文件中定义外部变量Num,然后在另一个文件中用关键字extern进行“外部变量声明”,即“extern Num”。 + + - 在编译和链接时,系统就会知道Num有外部链接,可以从别处找到已定义的外部变量Num,并将另一个文件中定义的外部变量Num的作用域扩展到本文件,那么就可以在本文件中合法的使用变量Num了。 + + - 例子:分别编写两个源文件文件file1和file2,在file1中定义外部变量A,在file2中用extern来声明外部变量,把A的作用域扩展到file2中 + +file1: +```c + //file1 + #include + //给定b的值,输入a和m,求a*b和a**m(a的m次方)的值 + + int A; //定义外部变量 + int power(int); + int main() + { + int b = 3, c, d, m; + printf("input a and its power m:"); + scanf_s("%d %d", &A, &m); + c = A * b; + printf("%d*%d=%d\n", A, b, c); + d = power(m); + printf("%d ** %d=%d\n", A, m, d); + system("pause"); + } +``` +file2: +```c + //file2 + extern A; + //把在file1文件中已定义的外部变量的作用域扩展到本文件 + int power(int n) + { + int i, y = 1; + for ( i = 1; i <= n; i++) + { + y *= A; + } + + return y; + } +``` +运行结果: + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210510175612.png) + +- 解析: + - 假设某一程序有5个源文件,那么只需要在其中一个源文件中定义外部变量A,然后在其余四个文件中使用关键字extern声明外部变量即可。各文件经过编译后会连接成一个可执行的目标文件。 + - 用这种方法扩展全局变量的作用域应十分慎重,因为在执行一个文件中的操作时可能会改变该全局变量的值,这样就会影响到另一个文件中全局变量的值,从而影响该文件中函数的执行结果。 + +- 将外部变量的作用域限制在本文件中 + - 若希望外部变量仅限于被本文件使用,而不被其它文件使用,那么可以在定义外部变量时加上一个static,例如: + +```c +static int A; +int main() +{ + ...... +} +``` +这样在其它文件中就算使用“extern A”,也不能使用本文件的外部变量A。 +这种加上static声明,只能用于本文件的外部变量成为“静态外部变量”。 +用static声明一个变量的作用: + +(1)对局部变量用static声明,把它分配在静态存储区,该变量在整个程序执行期间所在的存储单元都不会释放。 + +(2)对全局变量用static声明,则该变量的作用域只限于本文件模块(即被声明的文件中) + +## 8. 存储类别小结 + +对数据的定义,需要指定两种属性:**数据类型**和**存储类别**,分别使用两个关键字。 + +例如: + +```C +static int a; //静态局部整型变量或静态外部整型变量 +auto char c; //自动变量,在函数内定义使用 +register int d; //寄存器变量,在函数内定义 +``` + + + +此外,可以用`extern`声明已定义的外部变量,例如: + +```c +extern b; //将已定义的外部变量b的作用域扩展至此 +``` + +**下面从不同角度做些归纳** + +1. 从作用域角度分,有局部变量和全局变量。它们采用的存储类型如下: + + + + ![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210510211854.png) + + + +2. 从变量存在的时间(生存期)来区分,有动态存储和静态存储两种类型。静态存储类型是整个程序运行时间都存在,而动态存储原则是在调用函数时临时分配分配单元。 + + + + ![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210510222624.png) + +3. 从变量值存放的位置来区分,可分为: + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210510233802.png) + +************ + +4. 关9.作用域和生存期的概念。 + +- 如果一个变量在某个文件或函数范围内是有效的,就称为该范围为该变量的**作用域**。在此作用域内可以引用该变量,在专业书中称变量在此作用域内 “可见” ,这种性质称为变量的可见性。 + +- 如果一个变量值在某一时刻是存在的,则认为这一时刻属于该变量的生存期,或称该变量在此时刻 “存在” 。 + +**各种类型变量的作用域和存在性情况** + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210511000340.png) + +5. `static`对局部变量和全局变量的作用域不同。对于局部变量来说它使变量的由动态存储方式改变为静态存储方式。而对于全局变量来说,它使变量局部化(局部于本文件),但静态存储方式。从作用域角度看,但凡有`static`声明的,其作用域都是局限的或者局限于本函数内(静态局部变量),或者局限于本文件内(静态外部变量)。 + +## 9. 内部函数和外部函数 + +根据函数能否被其他源文件调用,将函数区分为**内部函数**和**外部函数**。 + +**内部函数** + +如果一个函数只能被本文件中其他函数所调用,它将称为**内部函数**。 + +在定义内部函数时,在函数名和函数类型前面加`static`,即: + +```c +static 类型名 函数名(形参表); +``` + +内部函数又称**静态函数**,因为它是`static`声明的。 + +**外部函数** + + 如果在定义函数时,在函数首部的最左端加关键字`extern`,则此函数是外部函数,可供其他文件调用。 + +一般形式为: + +```c +extern 类型名 函数名(形参表); +``` + +C语言规定,如果在定义函数时省略`extern`,则默认为外部函数。 + +在需要调用此函数的其他文件中,需要对此函数作声明(不要忘记,即使在本文件中调用一个函数,也需要用函数原型进行声明)。在对此函数作声明时,要加关键字`extern`,表示该函数 “是在其他文件中定义的外部函数” 。 + diff --git "a/docs/50.C\350\257\255\350\250\200/06.\347\254\254\344\272\224\347\253\240\346\214\207\351\222\210.md" "b/docs/50.C\350\257\255\350\250\200/06.\347\254\254\344\272\224\347\253\240\346\214\207\351\222\210.md" new file mode 100644 index 00000000..e9c7d60f --- /dev/null +++ "b/docs/50.C\350\257\255\350\250\200/06.\347\254\254\344\272\224\347\253\240\346\214\207\351\222\210.md" @@ -0,0 +1,892 @@ +--- +title: 第五章 指针 +date: 2021-05-11 14:30:00 +permalink: /c/note5/ +author: + name: eric + href: https://wfmiss.cn +--- +# 第五章 指针【C语言的灵魂】 + +## 1. 指针及指针变量【概念、定义】 + +定义指针变量:`类型名称 *指针变量名;` + +**在定义指针变量时要注意:** + +1. 指针变量前面的“`*`”表示该变量为指针型变量。 +2. 在定义指针变量时必须**指定基类型。**指针的基类型用来定义此指针变量可以指向的变量的类型。一个变量的指针的含义包括两个方面,一是存储单元编号表示的纯地址,一是它指向的存储单元的数据类型(如int、char、float等)。 +3. 指向整型数据的指针类型表示为“`int*`”,读作**“指向int的指针”**或简称**“int指针”**。【`int*,float*,char*`,是三种不同的类型,不能混淆】 +4. 指针变量中只能存放地址(指针),不要将一个整数赋给指针变量。 + - 如:`*pointer_1=100; //pointer_1是指针变量,100是整数,不合法` + +如果需要取出某个变量的地址,可以使用取址运算符`&`: + +例如: + +```c +char *pa = &a; +int *pb = &b; +``` + +如果需要访问指针变量指向的数据类型,可以使用取值运算符`*`: + +例如: + +```c +printf("%c,%d\n",*pa,*pb); +``` + +******* + +访问地址里的值的两种方式: + +直接访问:即按变量名进行的访问。 + +间接访问:即通过指针变量进行的访问。 + +**注意**:避免访问未初始化的指针。【因为未初始化的指针指向的地址是随机的,未初始化就使用是非常危险的!!!】 + +例如:【以下示例为**错误的**】 + +```c +#include +main(){ + int *a; + *a = 123; +} +``` + +**指针与指针变量** + +如果有一个**变量**专门来存放另一变量的**地址(即指针)**,则称它为**“指针变量”**。 + +指针变量就是地址变量,用来存放地址,**指针变量的值就是地址(即指针)。** + +**注意:**区分 “指针” 和 “指针变量” 这两个概念。**指针就是一个地址,而指针变量是存放地址值的变量。** + +## 2. 引用指针变量 + +在引用指针变量时,可能有3种情况: + +- 给指针变量赋值。 + - 如:`p = &a; //把 a 的地址赋给指针变量 p `。 + - 指针变量p的值是变量a的地址,p指向a。 +- 引用指针变量指向的变量。 + - 如果已经执行`p=&a;`,即指针变量p指向了整型变量a,则`printf("%d",*p);` + - 其作用是以整数形式输出变量p指向的变量的值,即变量a的值。 +- 引用指针变量的值。 + - 如:`printf("%o",p);` + - 其作用是以八进制整数输出指针变量的值,如果p指向变量a,就是输出了a的地址,即&a。 + +**注意:要熟练掌握两个有关运算符。** + +- `&` 取地址运算符。&a是变量a的地址。 +- `*` 指针运算符(或称“间接访问”运算符),`*p`代表指针变量p指向的对象。 + +***** + +**指针变量作为函数参数** + +函数的参数不仅可以是整数型、浮点型、字符型等数据,还可以是指针类型。它的作用是将一个变量的地址传送到另一个函数中。 + +注意:不能企图通过改变指针形参的值而使指针实参的值改变。 + +***** + +## 3. 通过指针引用数组 + +指针变量既然可以指向变量,当然也可以指向数组元素(把某一元素的地址放到一个指针变量中)。所谓数组元素的指针就是数组元素的地址。 + +将数组元素地址赋值给指针变量,如: + +```c +int a[10]={1,3,5,7,9,11,13,15,17,19};//定义a为包含10个整型数据的数组 +int *P; //定义p为指向整型变量的指针变量 +p = &a[0]; //把a[0]元素的地址赋给指针变量p +``` + +下标法赋值: + +`指针变量 = &数组名[数值];` 将下表为 `数组名[数值]` 的元素地址,赋值给 `指针变量`。 + +不加标赋值: + +`指针变量 = 数组名;`将数组的首元素【即`数组名[0]`】地址赋值给`指针变量`。 + +下面两个语句等价: + +```c +int *p; + p = &a[0]; +------------------- +int *p; +p = a; +``` + +**引用数组元时指针的运算** + +在指针已指向一个数组元素时,可以对指针进行以下运算: + +- 加一个整数(用`+或+=`),如`p+1`; +- 减一个整数(用`-或-=`),如`p-1`; +- 自加运算,如:`p++; ++p;` +- 自减运算,如:`p--; --p;` + +两指针相减,如:`p1-p2`(只有p1和p2都指向同一数组中的元素时才有意义)。 + +分别说明如下: + +- 如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素,p-1指向同一数组元素中的上一个元素。 + - 注意:执行p+1时并是将p的值(地址)简单的加1,而是加上一个数组元素所占的字节数。 + - 例如:数组元素是float型,每个元素是float型,每个元素占4个字节,则p+1意味着使p的值(地址)加4个字节,以使它指向下一元素。 +- 如果p的初值为`&a[0]`,则表示`p+i`和`a+i`就是数组元素`a[i]`的地址。 +- `*(p+i)`或`*(a+i)`是`p+i`或`a+i`所指向的数组元素,即`a[i]`。 + - 说明:`[]`实际上是变址运算符,即将`a[i]`按`a+i`计算然后找出此地址单元中的地址。 +- 如果指针变量`p1`和`p2`都指向同一组数组中的元素,如执行`p2-p1`,结果是`p2-p1`的值(两个地址之差)除以数组元素的的长度。 + - 注意:两个地址不能相加,如`p1+p2`是无实际意义的。 + +******* + +**通过指针引用数组元素** + +引用一个数组元素,可以用下面两种方法: + +- 下标法:如`a[i]`形式; +- 指针法:如`*(a+i)`或`*(p+i)`。其中 `a` 是数组名,`p` 是指向数组元素的指针变量,其初值`p=a`。 + +指向数组元素的指针变量也可以带下标,如`p[i]`。 + +`++` 和 `*` 同优先级,结合方向为自左向右。 + +`*(p++)与*(++p)`,作用不相同。 + +- `*(p++)`:是先取`*p`的值,然后使`p+1`。 +- `*(++p)`:是先`p+1`,然后再取`*p`的值。 + +`++(*p)`:表示`p`所指向的元素值加1。 + +`--(*p)`:表示`p`所指向的元素值减1。 + +所以: + +- `*(++p)`相当与`a[++i]`,先使p自加,再进行`*`运算。 +- `*(--p)`相当与`a[--i]`,先使p自减,再进行`*`运算。 + +**用数组名做函数参数** + +数组名做函数参数方法定义一般形式为: + +```C +返回值类型 方法名(参数类型 数组名[],参数列表……){ + 方法体; + 返回值; +} +``` + +指针做函数参数定义一般形式为: + +```C +返回值类型 方法名(参数类型 *数组名,参数列表……){ //这里的 “*数组名” 表示数组的首元素地址 “数组名[0]” + 方法体; + 返回值; +} +``` + +两种定义方法等价。 + +`*数组名`等价于`数组名[0]`。 + +**注意:**数组名做方法参数时,传递的是数组首元素的地址,而非元素值。 + +常用这种方法通过调用一个函数来改变实参数组的值。 + +*以表变量名和数组名作为函数参数的比较* + +| 参数类型 | 变量名 | 数组名 | +| ---------------------------- | -------------------- | ------------------ | +| 要求的形参类型 | 变量名 | 数组名或指针 | +| 传递参数 | 变量的值 | 实参数组首元素地址 | +| 通过函数调用能否改变实参的值 | 不能改变实参变量的值 | 能改变实参数组的值 | + +**注意:**实参数组名代表一个固定的地址,或者说是指针常量,但形参数组名并不是一个固定的地址,而是按指针变量处理。 + +在函数调用进行虚实结合后,形参的值就是实参数组首元素的地址。在函数执行期间,它还可以再被赋值。 + +**归纳分析:**如果有一个实参数组,想要在函数中改变此数组中的元素的值,实参与形参的对应关系有以下4种情况。 + +- 形参和实参都用数组名。 + - 例如: + +```c +int main(){ + int a[10]; + ... + f(a,10); + ... +} +int f(int x[],int n){ + ... +} +//由于形参数组名x接收了实参数组首元素a[0]的地址值,因此可以认为在函数调用期间,形参数组与实参数组共用一段内存单元。 +``` + +- 实参用数组名,形参用指针变量。 + - 例如: + +```c +int main(){ + int a[10]; + ... + f(a,10); + ... +} +void f(int *x,int n){ + ... +} +//实参a为数组名,形参数组x为int * 型的指针变量,调用函数开始后,形参x指向a[0],即x=&a[0]。通过x的值改变,可以指向a数组的任一元素。 +``` + +- 实参形参都用指针变量。 + - 例如: + +``` c +int main(){ + int a[10], *p = a; + ... + f(p,10); + ... +} +void f(int *x,int n){ + ... +} +//实参p和形参x都是int * 型的指针变量。先使实参指针变量p指向数组a[0],p的值是&a[0]。然后将p的值传给指针变量x,x的初始值也是&a[0],通过x值的改变可以使x指向数组元素a的任一元素。 +``` + +- 实参为指针变量,形参为数组名。 + - 例如: + +```c +int main(){ + int a[10], *p = a; + ... + f(p,10); + ... +} +void f(int x[],int n){ + ... +} +//实参p为指针变量,它指向a[0]。形参为数组名x,编译系统把x作为指针变量处理,今将a[0]的地址传给形参x,使x也指向a[0]。也可以理解为形参数组x和a数组共用同一段内存单元。在函数执行过程中可以使x[i]的值发生变化,而x[i]就是a[i]。 +``` + +**注意:**如果使用指针变量作实参,必须先使指针变量有确定值,指向一个以定义的对象。 + +以上4种方法,实质上都是地址的传递。其中(3)、(4)两种只是形式上的不同,实际上形参都是使用指针变量。 + +**通过指针引用多维数组** + +指针引用多维数组:除了表示取元素之外,还可以表示取哪一维 + +对于二维数组: +1、 + + + +`a`是一个行指针。指向一个有四个元素的数组,占16个字节 +`&a`是一个指向二维数组的指针,二维数组有12个元素,占48个字节 +`*a`是一个指向int类型数据的指针。 + +2、 +`a[i][j]`等价于`*((a+i)+j)`,` &a[i][j]`等价于`(a+i)+j` +`a[i]`等价于`*(a+i)`,` &a[i]` + + + +3、 二维数组名是指向行的,它不能对如下说明的指针变量p直接赋值: + +```c +int a[3][4]={{10,11,12,13},{20,21,22,23},{30,31,32,33}},*p; +``` + + +其原因就是p与a的对象性质不同,或者说二者不是同一级指针。C语言可以通过定义行数组指针的方法,使得一个指针变量与二维数组名具有相同的性质。 + +行数组指针的定义方法如下: +`数据类型 (*指针变量名)[二维数组列数];` + +例如,对上述a数组,行数组指针定义如下: +`int (p)[4];`它表示,数组p有4个int型元素,分别为`(*p)[0]、(*p)[1]、(*p)[2]、(*p)[3] `,亦即p指向的是有4个int型元素的一维数组,即p为行指针 + +此时,可用如下方式对指针p赋值: +`p=a;` + +***** +**指针访问三维数组** +数组与指针关系密切,数组元素除了可以使用下标来访问,还可用指针形式表示。数组元素可以很方便地用数组名常指针来表示,以3维int型数组A举例,其中的元素A[i][j][k]可用下述形式表示: + +(1)`*(A[i][j]+k)` +`A[i][j]`是int型指针,其值为`&A[i][j][0]`,因此,`A[i][j][k]`可表述为`*(A[i][j]+k)`。 +(2)`*(*(A[i]+j)+k)` +和第一种形式比较,不难发现`A[i][j]= *(A[i]+j)`,`A[i]`是二级指针,其值为&`A[i][0]`。 +(3)`*(*(*(A+i)+j)+k)` +将第2种形式的A[i]替换成了`*(A+i)`,此处A是三级指针,其值为`&A[0]`。 +此处以3维数组举例,还可进一步推广到更高维的情况。 + +****** +**指针数组** +指针也可作为数组中的元素,将一个个指针用数组形式组织起来,就构成了指针数组。 + +一个数组,若其元素均为指针类型数据,称为指针数组,也就是说,指针数组中的每一个元素都存放一个地址,相当于一个指针变量。 + +定义一维指针数组的一般形式为: +```c +类型名 *数组名[数组长度]; +int *p[4]; +``` + +***** + +**用指向数组的指针作函数参数** + +一维数组名可以做函数参数,多维数组名可以做函数参数。用指针变量作形参,以接受实参数组名传递过来的地址。 + +可以有两种方法: + +- 用指向变量的指针变量。 +- 用指向一维数组的指针变量。 + +## 4. 通过指针引用字符串 + +字符串的应用方式: + +- 用字符数组存放一个字符串,可以通过数组名和下标引用字符串中的一个字符,也可以通过数组名和格式声明`%s`输出该字符串。 + +```c +#include +main(){ + char *string; + string="I love you"; + printf("%s\n",string); + + char string2[]="I love you"; + printf("%s\n",string2); + + char string3[]={"I love you"}; + printf("%s\n",string3); +} +//三种定义形式输出结果一样 +``` + +- 用指针变量访问字符串。通过改变指针变量的值使它指向字符串中的不同字符。 + +**使用字符串指针变量和字符数组的比较** + +1. 字符串由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第一个字符的地址),绝不是将字符串放到字符指针变量中。 +2. 赋值方式。可以对字符串指针变量赋值,但不能对数组名赋值。 +3. 初始化定义。对字符指针变量赋初值: + +```c +char *a="I love china!"; +//等价于 +char *a; +a = "I Love china!"; +//而对数组的初始化: +char str[14]="I love china!"; +//不等价于 +char str[14]; +str[]="I love china!"; +``` + +数组可以在定义时对各元素赋初值,但不能用赋值语句对字符串数组中全部元素整体赋值。 + +4. 存储单元的内容。编译时为字符数组分配若干存储单元,以存放各元素的值,而对字符指针变量,只分配一个存储单元。 + - 如果定义了字符数组,但未能对它赋值,这时数组中的元素的值是不可预料的。可以引用(如输出)这些值,结果显然是无意义的,但不会造成严重的后果,容易发现和更正。 + - 如果定义了字符指针变量,应当及时把字符变量(或字数组元素)的地址赋给它,使它指向一个字符型数据,如果未对它赋予一个地址值,它并未具体指向一个确定的对象。此时如果向该指针变量指向的对象输入数据,可能会出现严重的后果。 +5. 指针变量的值是可以改变的,而字符数组名代表一个固定的值(数组首元素的地址),不能改变。 +6. 字符数组中各元素的值是可以取代的(可以对它们在赋值),但字指针变量指向的字符串常量中的内容是不可以被取代的(不能对他它们在赋值)。 +7. 引用数组元素。对字符数组可以用下标法(用数组名和下标)引用一个数组元素,也可以用地址法引用数组元素。 +8. 用指针变量指向一个格式字符串,可以用它代替printf函数中的格式字符串。 + +```c +char *format; +format = "a=%d,b=%f\n";//使format指向一个字符串 +printf(format,a,b); +//这种printf函数称为可变格式输出函数。 +``` + +**字符指针做函数参数** + +实参和形参都可以选择字符数组名和字符指针变量,但存在区别: +(1)编译时为字符数组分配若干存储单元,以存放个元素的值,而对字符指针变量,只分配一个存储单元 +(2)指针变量的值是可以改变的,而数组名代表一个固定的值(数组首元素的地址),不能改变 + +```c +char *a="i am a student" +a=a+7; //合法的 + +char str[]={"i am a student"}; +str=str+7 //非法的 +``` + +(3)字符数组中各元素的值是可以改变的,但字符指针变量指向的字符串常量中的内容是不能改变的 + +```c +char a[]="house"; +char *b="house"; +a[2]='r'; //合法 +*(b+2)='r'; //非法 +``` + +接着,引入一个用字符数组名作为函数参数的例子,实现字符串的复制 + +```c +#include +int main(){ + void copy_string(char from[] ,char to[]); + char a[]="i am a teacher"; + char b[]="you are a student"; + + copy_string(a,b); //把a复制到b + printf("%s\n%s",a,b); +} +void copy_string(char from[], char to[]){ + int i=0; + while(from[i]!='\0'){ + to[i]=from[i]; i++; + } + to[i]='\0'; +} +``` + +## 5. 指向函数的指针 + +函数名就是函数的指针,它代表函数的起始地址。 + +**定义和使用指向函数的指针变量** + +定义指向函数的指针变量的一般形式为: + +`类型名 (*指针变量名)(函数参数列表)` + +这里的 “类型名” 是指函数的返回类型。 + +**说明:** + +1. 定义指向函数的指针变量,并不意味着这个指针变量可以指向任何函数,它只能指向在定义时指定的类型的函数。 + - 在程序中把哪一个函数的地址赋给它,它就指向哪一个函数。在一个程序运行中,一个指针变量可以先后指向同类型的不同函数。 +2. 如果要用指针调用函数,必须先使用指针变量指向该函数。 + - 如:`指针变量名 = 函数名 ;`这样就把 “函数名” 的入口地址赋给了指针变量 “指针变量名“ 。 +3. 在给函数指针变量赋值时,只须给出函数名而不必给出参数。 +4. 用函数指针变量调用函数时,只需将(`*指针变量名`)代替函数名即可,在(`*指针变量名)之后的括号中根据需要写上实参。 +5. 对指向函数的指针变量不能进行算数运算,如`p+n,p++,p--`等运算是无意义的。 +6. 用函数名调用函数,只能调用所指定的一个函数,而通过指针变量比较灵活,可以根据不同情况先后调用不同的函数。 + +****** + +**用指向函数的指针作函数参数** + +指向函数的指针变量的一个重要用途是把函数的入口地址作为参数传递到其他函数。 + +指向函数的指针可以作为函数参数,把函数的入口地址传递给形参,这样就能够在被调用的函数中使用实参函数。 + +它的原理简述如下: + +有一个函数(假设函数名为fun),它有两个形参(x1和x2),定义x1和x2为指向函数的指针变量。再调用函数fun时,实参为两个函数名f1和f2,给形参传递的是f1和f2的入口地址。这样在函数fun中就可以调用f1和f2函数了。 + +例如: + +```c +实参函数名 f1 f2 +void fun(int (*x1)(int),int (*x2)(int,int))//定义fun函数,形参是指向函数的指针变量 +{ + int a,b,i=3,j=5; + a=(*x1)(i); //调用f1函数,i是实参 + b=(*x2)(i,j); //调用f2函数,i、j是实参 +} +``` + +在fun函数中声明形参x1和x2为指向函数的指针变量,x1指向的函数有一个整型形参,x2指向的函数有两个整型实参。函数fun的形参x1和x2(指针变量)在函数fun未被调用时并不占内存单元,也不指向任何函数。在主函数调用fun函数时,把实参函数f1和f2的入口地址传给形参指针变量x1和x2,使x1和x2指向函数f1和f2。这时,在函数fun中,用`*x1`和`*x2`就可以调用函数f1和f2。`(*x1)(i)`就相当于`f1(i)`,`(*x2)(i,j)`就相当于`f2(i,j)`。 + +## 6. 返回指针值的函数 + +定义返回指针的函数的原型一般形式为: + +```c +类型名 *函数名(参数列表); +``` + +例如: + +```c +int *a(int x,int y); +``` + +a是函数名,调用它以后能得到一个`int*`型(指向整型数据)的指针,即整型数据的地址。x和y是函数a的形参,为整型。 + +请注意在`*a`两侧没有括号,在a的两侧分比是`*`运算符和`()`运算符。而`()`优先级高于`*`,因此a先与`()`结合,显然这是函数形式。这个函数前面有一个`*`,表示此函数是指针型函数(函数值是指针)。最前面的`int`表示返回的指针指向整型变量。 + +## 7. 指针数组和多重指针 + +定义一维指针数组的一般形式为: + +```c +类型名 *数组名[数组长度]; +``` + +类型名中应包括符号`*`,如`int*`表示指向整数数据的指针类型。 + +例如: + +```c +int *p[4]; +``` + +由于`[]`比`*`优先级高,因此p先与`[4]`结合,形成`p[4]`形式,表示p数组有4个元素。然后再与p前面的`*`结合,`*`表示此数组是指针类型的,每个数组元素(相当于一个指针变量)都指向一个整型变量。 + +注意一定不要写成: + +```c +int (*p)[4]; //这是指向一维数组的指针变量 +``` + +**指向指针数据的指针变量** + +定义一个指向指针数据类型的指针变量: + +```c +char **p; +``` + +p的前面有两个`*`号。`*`运算符的结合性是从右到作,因此`**p`相当于`*(*p)`,显然`*p`是指针变量的定义形式。如果没有最前面的`*`,那就是定义了一个指向字符数据的指针变量。现在它前面又有一个`*`号,即`char**p`。可以把它分成两部分看,即:`char*`和`( *p)`,后面的`( *p)`表示`P`是指针变量,前面的`char*`表示`p`指向的是`char*`型的数据。也就是说,`P`指向一个字符指针变量(这个字符指针变量指向一个字符型数据)。 + +例如:使用指向指针数据的指针变量。 + +```C +#include +main(){ + char *name={"Follw me","BASIC","Great Wall","FORTRAN","Computer design"}; + char **p; + int i; + for(i=0;i<5;i++){ + p=name+i; + printf("%s\n",**p); + } +} +``` + +**指针数组作main函数的形参** + +main函数的第1行一般写成以下形式: + +`int main()`或`int main(void)` + +括号中是空的或有“void”,表示main函数没有参数,调用main函数时不必给出实参。 + +这是一般程序常采用的形式。实际上,在某些情况下,main函数可以有参数,即: + +```c +int main(int arge,char * argv[]) +``` + +其中,`argc`和`argv`就是main函数的形参,它们是程序的“命令行参数”。 + +`arge` ( `argument count`的缩写,意思是参数个数) ,argv`(`argument vector` 缩写,意思是参数向量),它是一个*char指针数组,数组中每一个元素(其值为指针)指向命令行中的-个字符串的首字符。 + +**注意:**如果用带参数的main函数,其第一个形参必须是int 型,用来接收形参个数,第二个形参必须是字符指针数组,用来接收从操作系统命令行传来的字符串中首字符的地址。通常main函数和其他函数组成一个文件模块,有一个文件名。对这个文件进行编译和连接,得到可执行文件(后缀为.exe)。用户执行这个可执行文件,操作系统就调用main函数,然后由main函数调用其他函数,从而完成程序的功能。 + +什么情况下main函数需要参数?main函数的形参是从哪里传递给它们的呢? + +显然形参的值不可能在程序中得到。main函数是操作系统调用的,实参只能由操作系统给出。在操作命令状态下,实参是和执行文件的命令一起给出的。例如在DOS,UNIX或Linux等系统的操作命令状态下,在命令行中包括了命令名和需要传给main函数的参数。 + +命令行的一般形式为: + +```txt +命令名 参数1 参数2 ... 参数n +``` + +命令名和各参数之间用空格分隔。 + +## 8. 动态内存分配与指向它的指针变量 + +对内存动态分配是通过系统提供的函数库来实现的,主要有 molloc ,calloc ,free ,realloc 这4个函数。 + +- 用**malloc**函数开辟动态存储区: + +其函数原型为: + +```c +void * malloc(unsigned int size); +``` + +其作用是在内存的动态存储区中分配一个长度为size 的连续空间。形参size的类型定为无符号整型(不允许为负数)。 + +此函数的值(即“返回值")是所分配区域的第一个字节的地址,或者说,此函数是一个指针型函数,返回的指针指向该分配域的第一个字节。如: + +```c +malloc( 100); //开辟100字节的临时分配域.函数值为其第1个字节的地址 +``` + +注意指针的基类型为void,即不指向任何类型的数据,只提供一个纯地址。 + +如果此函数未能成功地执行(例如内存空间不足),则返回空指针(NULL)。 + +- 用**calloc**函数开辟动态存储区: + +其函数原型为: + +```c +void * calloc(unsigned n,unsigned size); +``` + +其作用是在内存的动态存储区中分配n个长度为size 的连续空间,这个空间一般比较大,足以保存一个数组。 + +用calloc函数可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。这就是动态数组。函数返回指向所分配域的第一个字节的指针;如果分配不成功,返回NULL。 + +如: + +```c +p= calloc(50.4); //开辟50X4个字节的临时分配域,把首地址赋给指针变量p +``` + +- 用**realloc**函数重新分配动态存储区 + +其函数原型为: + +```c +void * realloc(void * p,unsigned int size); +``` + +如果已经通过 malloc 函数或 calloc 函数获得了动态空间,想改变其大小,可以用 recalloc 函数重新分配。 + +用realloc函数将p所指向的动态空间的大小改变为size。p的值不变。如果重分配不成功,返回NULL。 + +如: + +```c +realloc(p,50); //将p所指向的已分配的动态空间改为50字节 +``` + +- 用free函数释放动态存储区 + +其函数原型为: + +```c +void free(void * p); +``` + +其作用是释放指针变量p所指向的动态空间,使这部分空间能重新被其他变量使用。p应是最近一次调用calloc或malloc函数时得到的函数返回值。 + +如: + +```c +free(p); //释放指针变量p所指向的已分配的动态空间 +``` + +free函数无返回值。 + +**注意:**以上4个函数的声明在`stdlib. h`头文件中,在用到这些函数时应当用`“# include”`指令把`stdlib.h`头文件包含到程序文件中。 + +**void指针类型** + +C99允许使用基类型为void的指针类型。可以定义一个基类型为void的指针变量(即`void*`型变量),它不指向任何类型的数据。请注意:不要把 “指向void类型” 理解为能指向 “任何的类型” 的数据,而应理解为 “指向空类型” 或 “不指向确定的类型“ 的数据。在将它的值赋给另一指针变量时由系统对它进行类型转换,使之适合于被赋值的变量的类型。 + +例如: + +```c +int a=3; //定义a为整型变量 + +int *p1=&a; //p1指向int型变量 + +char *p2; //p2指向char型变量 + +void *p3; //p3为无类型指针变量(基类型为void型) + +p3=(void*)p1; //将p1的值转换为void*类型,然后赋值给p3 + +p2= (char*)p3; //将p3的值转换为char*类型,然后赋值给p2 + +printf("%d",* p1); //合法,输出整型变量a的值 + +p3= &a; printf("%d",* p3); //错误,p3是无指向的,不能指向a +``` + +这种空类型指针在形式上和其他指针一样,遵循C语言对指针的有关规定,它也有基类型,只是它的基类型是void。 + +可以这样定义: + +```c +void * p; //定义p是void*型的指针变量 +``` + +`void*`型指针代表“无指向的地址”,这种指针不指向任何类型的数据。不能企图通过它存取数据,**在程序中它只是过渡性的,只有转换为有指向的地址,才能存取数据。** + +C 99这样处理,更加规范,更容易理解,概念也更清晰。 + +现在所用的一些编译系统在进行地址赋值时,会自动进行类型转换。 + +例如: + +```c +int * pt; +pt= (int*)mcaloc(100); //mcaloc(100)是void *型,把它转换为int*型 +可以简化为 +pt= mcaloc(100); //自动进行类型转换 +``` + +赋值时,系统会先把`mcaloc(100)`转换为的`pt`的类型,即`(int* )`型,然后赋给`pt`,这样`pt`就指向存储区的首字节,在其指向的存储单元中可以存放整型数据。 + +## 9. 有关指针的小结 + +(1)首先要准确理解指针的含义。“指针”是C语言中一个形象化的名词,形象地表示“指向”的关系,其在物理上的实现是通过地址来完成的。正如高级语言中的“变量”,在物理上是“命名的存储单元”。Windows中的“文件夹”实际上是“目录”。离开地址就不可能弄清楚什么是指针。明确了“指针就是地址”,就比较容易理解了,许多问题也迎办而解了。 + +例如: + +- `&a`是变量`a`的地址,也可称为变量`a`的指针。 + +- 指针变量是存放地址的变量,也可以说,指针变量是存放指针的变量。 + +- 指针变量的值是一个地址,也可以说,指针变量的值是- 一个指针。 + +- 指针变量也可称为地址变量,它的值是地址。 + +- `&`是取地址运算符,`&a`是`a`的地址,也可以说,`&`是取指针运算符。`&a`是变量`a`的指针(即指向变量`a`的指针)。 + +- 数组名是一个地址,是数组首元素的地址,也可以说,数组名是一个指针,是数组首元素的指针。 + +- 函数名是一个指针(指向函数代码区的首字节),也可以说函数名是一个地址(函数代码区首字节的地址)。 + +- 函数的实参如果是数组名,传递给形参的是一个地址,也可以说,传递给形参的是一个指针。 + + + +(2) 在C语言中,所有的数据都是有类型的,例如常量123并不是数学中的常数123,数学中的123是没有类型的,123和123.0是一样的,而在C语言中,所有数据都要存储在内存的存储单元中,若写成123,则认为是整数,按整型的存储形式存放,如果写成123.0,则认为是单精度实数,按单精度实型的存储形式存放。此外,不同类型数据有不同的运算规则。可以说,C语言中的数据都是“有类型的数据”,或称“带类型的数据”。 + +对地址而言,也是同样的,它也有类型,首先,它不是一个数值型数据,不是按整型或浮点型方式存储,它是按指针型数据的存储方式存储的(虽然在VisualC++中也为指针变量分配4个字节,但不同于整型数据的存储形式)。指针型存储单元是专门用来存放地址的,指针型数据的存储形式就是地址的存储形式。 + +其次,它不是一个简单的纯地址,还有一个指向的问题,也就是说它指向的是哪种类型的数据。如果没有这个信息,是无法通过地址存取存储单元中的数据的。所以,一个地址型的数据实际上包含3个信息: + +①表示内存编号的纯地址。 + +②它本身的类型,即指针类型。 + +③以它为标识的存储单元中存放的是什么类型的数据,即基类型。 + +例如:已知变量为`a`为`int`型,`&a`为`a`的地址,它就包括以上3个信息,它代表的是一个整型数据的地址,`int`是`&a`的基类型(即它指向的是`int`型的存储单元)。可以把②和③两项合成一项,如 “指向整型数据的指针类型” 或 “基类型为整型的指针类型” ,其类型可以表示为“`int*`”型。这样,对地址数据来说,也可以说包含两个要素:内存编号(纯地址)和类型(指针类型和基类型)。这样的地址是 “带类型的地址” 而不是纯地址。 + +(3)要区别指针和指针变量。指针就是地址,而指针变量是用来存放地址的变量。有人认为指针是类型名,指针的值是地址。这是不对的。类型是没有值的,只有变量才有值,正确的说法是指针变量的值是一个地址。不要杜撰出 “地址的值” 这样莫须有的名词。地址本身就是一个值。 + +(4)什么叫 “指向” ?地址就意味着指向,因为通过地址能找到具有该地址的对象。对于指针变量来说,把谁的地址存放在指针变量中,就说此指针变量指向谁。但应注意:并不是任何类型数据的地址都可以存放在同一个指针变量中的,只有与指针变量的基类型相同的数据的地址才能存放在相应的指针变量中。 + +例如: + +```c +int a,*P; //p是int关型的指针变量,基类型是int型 + +float b; + +p= &a; //a是int型,合法 + +p=&b; //b是float型,类型不匹配 +``` + +既然许多数据对象(如变量数组、字符串和函数等)都在内存中被分配存储空间,就有了地址,也就有了指针。可以定义一些指针变量,分别存放这些数据对象的地址,即指向这些对象。`void*`指针是一种特殊的指针,不指向任何类型的数据。如果需要用此地址指向某类型的数据,应先对地址进行类型转换。可以在程序中进行显式的类型转换,也可以由编译系统自动进行隐式转换。无论用哪种转换,读者必须了解要进行类型转换。 + +(5)要深入掌握在对数组的操作中正确地使用指针,搞清楚指针的指向。一维数组名代表数组首元素的地址,如: + +```c +int *p,a[10]; +p=a; +``` + +p是指向`int`型类型的指针变量,显然,p只能指向数组中的元素(`int`型变量),而不是指向整个数组。在进行赋值时一定要先确定赋值号两侧的类型是否相同,是否允许赋值。 + +对"`p=a;`" ,准确地说应该是: p指向a数组的首元素,在不引起误解的情况下,有时也简称为:p指向a数组,但读者对此应有准确的理解。同理,p 指向字符串,也应理解为p指向字符串中的首字符。 + +(6)有关指针变量的归纳比较 + +指针变量的类型及含义 + +| 变量定义 | 类型表示 | 含义 | +| -------------- | ----------- | --------------------------------------------------------- | +| `int i;` | `int` | 定义整型变量 | +| `int *p;` | `int *` | 定义p为指向整型数据的指针变量 | +| `ina a[5];` | `int [5]` | 定义整型数组a,它有5个元素 | +| `int *p[4];` | `int * [4]` | 定义指针数组p,它由4个指向整型数据的指针元素组成 | +| `int (*p)[4];` | `int(*)[4]` | p为指向包含4个元素的一维数组的指针变量 | +| `int f();` | `int ()` | f为返回整型函数值的函数 | +| `int *p();` | `int * ()` | p为返回一个指针的函数,该指针指向整型数据 | +| `int(*p)();` | `int(*)()` | p为指向函数的指针,该函数返回一个整型值 | +| `int **p;` | `int **` | p是一个指针变量,它指向一个指向整型数据的指针变量 | +| `void *p;` | `void *` | p是一个指针变量,基类型为void(空类型),不指向具体的对象 | + +为便于比较,在表中包括了其他一些类型的定义。 + + + +(7)指针运算。 + +①指针变量加(减)一个整数。 + +例如: `p++`,`p--`,`p+i`,`p-i`,`p+=i`,`p-=i`等均是指针变量加(减)一个整数。 + +将该指针变量的原值(是一个地址)和它指向的变量所占用的存储单元的字节数相加(减)。 + +②指针变量赋值。 + +将一个变量地址赋给一个指针变量。 + +例如: + +```c +p= &a; //(将变量a的地址赋给p) + +P= array; //(将数组array首元素地址赋给p) + +p= &array[i]; //(将数组array 第i个元素的地址赋给p) + +p= max; //(max为已定义的函数.将max的人口地址赋给p) + +p1= p2; //(pl和p2是基类型相同指针变量,将p2的值赋给pl) + +``` + +注意:不应把一个整数赋给指针变量。 + +③两个指针变量可以相减。 + +如果两个指针变量都指向同一个数组中的元素,则两个指针变量值之差是两个指针之间的元素个数。 + +④两个指针变量比较。 + +若两个指针指向同一个数组的元素,则可以进行比较。指向前面的元素的指针变量“小于”指向后面元素的指针变量。如果p1和p2不指向同一数组则比较无意义。 + + + +(8)指针变量可以有空值,即该指针变量不指向任何变量,可以这样表示:`p= NULL;` + +其中,NULL是一个符号常量,代表整数0。在`stdio.h`头文件中对NULL进行了定义:`#define NULL 0` + +它使p指向地址为0的单元。系统保证使该单元不作它用(不存放有效数据)。 + +应注意,p的值为NULL与未对p赋值是两个不同的概念。前者是有值的(值为0),不指向任何变量,后者虽未对p赋值但并不等于p无值,只是它的值是一个无法预料的值,也就是p可能指向一个事先未指定的单元。这种情况是很危险的。因此,在引用指针变量之前应对它赋值。 + +任何指针变量或地址都可以与NULL作相等或不相等的比较,例如: + +```c +if(p==NULL){ + ... +} +``` + +指针是C语言中很重要的概念,是C的一个重要特色。 + +使用指针的优点: + +①提高程序效率; + +②在调用函数时当指针指向的变量的值改变时,这些值能够为主调函数使用,即可以从函数调用得到多个可改变的值; + +③可以实现动态存储分配。 + +同时应该看到,指针使用实在太灵活,对熟练的程序人员来说,可以利用它编写出颇有特色、质量优良的程序,实现许多用其他高级语言难以实现的功能,但也十分容易出错,而且这种错误往往比较隐蔽。指针运用的错误可能会使整个程序遭受破坏,比如由于未对指针变量`p`赋值就向`* p`赋值,就可能破坏了有用的单元的内容。如果使用指针不当,会出现隐蔽的、难以发现和排除的故障。因此,使用指针要十分小心谨慎,要多上机调试程序,以弄清一些细节,并积累经验。 + diff --git "a/docs/50.C\350\257\255\350\250\200/07.\347\254\254\345\205\255\347\253\240\350\207\252\345\256\232\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/docs/50.C\350\257\255\350\250\200/07.\347\254\254\345\205\255\347\253\240\350\207\252\345\256\232\346\225\260\346\215\256\347\261\273\345\236\213.md" new file mode 100644 index 00000000..645d7996 --- /dev/null +++ "b/docs/50.C\350\257\255\350\250\200/07.\347\254\254\345\205\255\347\253\240\350\207\252\345\256\232\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -0,0 +1,685 @@ +--- +title: 第六章 自定数据类型 +date: 2021-05-11 14:30:00 +permalink: /c/note6/ +author: + name: eric + href: https://wfmiss.cn +--- +# 第六章 自定数据类型 + +## 1. 定义和使用结构体变量 + +由不同类型数据组成的的组合型的数据结构,它称为结构体(`structre`)。 + +声明一个结构体类型的一般形式为: + +```c +struct 结构体名{ + 成员列表 +}; +``` + +`struct`是声明结构体类型时必须使用的关键字,不能省略。 + +注意:结构体类型的名字是由一个关键字`struct`和结构体名组成的。结构体名是由用户指定的,又称“结构体标记”(structure tag),以区别于其他结构体类型。上面的结构体声明中Student 就是结构体名(结构体标记)。 + +花括号内是该结构体所包括的子项,称为结构体的成员(member)。对各成员都应进行类型声明,即: + +```c +类型名 成员名; +``` + +“成员表列”(member list)也称为“域表”(field list),每一个成员是结构体中的一个域。成员名命名规则与变量名相同。 + +**说明:** + +(1)结构体类型并非只有一种,而是可以设计出许多种结构体类型,还可以根据需要建立结构体类型,各自包含不同的成员。 + +(2)成员可以属于另一个结构体类型。 + +例如: + +```c +struct Date //声明一个结构体类型struct Date +{ + int month; //月 + int day; //日 + int year; //年 +}; +struct Student //声明一个结构体类型struct Student +{ + int num; + char name[20]; + char sex; + int age; + struct Date birthday; //成员birthday属于struct Date类型 + char addr[30]; +}; +``` + +**定义结构体类型变量** + +1. 先声明结构体类型,再定义该类型的变量 + +上面已声明了一个结构体类型`struct Student`,可以用它来定义变量。例如: + +```c +struct Student student1,student2; +------------- -------- -------- + | | | +结构体类型名 结构体变量名 +``` + +这种方式是声明类型和定义变量分离,在声明类型后可以随时定义变量,比较灵活。 + +2. 在声明类型的同时定义变量 + +例如: + +```c +struct Student +{ + int num; + char name[20]; + char sex; + int age; + float score; + char addr[30]; +}studentl,student2; +``` + +它的作用与第一种方法相同,但是在定义struct Student 类型的同时定义两个struct Student类型的变量studentl 和student2。 + + 这种定义方法的一般形式为: + +```c +struct 结构体名{ + 成员列表 +}变量名表列; +``` + +声明类型和定义变量放在一起进行,能直接看到结构体的结构,比较直观,在写小程序时用此方式比较方便,但写大程序时,往往要求对类型的声明和对变量的定义分别放在不同的地方,以使程序结构清晰,便于维护,所以一般不多用这种方式。 + +3. 不指定类型名而直接定义结构体类型变量 + +其一般形式为: + +```c +struct{ + 成员表列 +}变量名表列; +``` + +指定了一个无名的结构体类型,它没有名字(不出现结构体名)。显然不能再以此结构体类型去定义其他变量。这种方式用得不多。 + +**说明:** + +(1)结构体类型与结构体变量是不同的概念,不要混淆。只能对变量赋值、存取或运算,而不能对一个类型赋值、存取或运算。在编译时,对类型是不分配空间的,只对变量分配空间。 + +(2)结构体类型中的成员名可以与程序中的变量名相同,但二者不代表同一对象。 例如,程序中可以另定义一个变量`num`,它与`struct Student `中的`num`是两回事,互不干扰。 + +(3)对结构体变量中的成员(即“域”),可以单独使用,它的作用与地位相当于普通变量。关于对成员的引用方法见下节。 + +**结构体变量的初始化和引用** + +在定义结构体变量时,可以对它初始化.即赋予初始值。然后可以引用这个变量,例如输出它的成员的值。 + +(1)在定义结构体变量时可以对它的成员初始化。初始化列表是用花括号括起来的一些常量,这些常量依次赋给结构体变量中的各成员。 + +注意:是对结构体变量初始化,而不是对结构体类型初始化。 + +`C99`标准允许对某一成员初始化,如: + +```c +struct Student b= {.name=' "Zhang Fang '};//在成员名前有成员运算符"." +``` + +`“. name”`隐含代表结构体变量`b`中的成员`b.name`。其他未被指定初始化的数值型成员被系统初始化为0,字符型成员被系统初始化为`'\0'`,指针型成员被系统初始化为NULL。 + +(2)可以引用结构体变量中成员的值,引用方式为: + +```c +结构体变量名.成员名 +``` + +`“.”`是成员运算符,它在所有的运算符中优先级最高,因此可以把`b.name`作为一个整体来看待,相当于一个变量。 + +注意:不能企图通过输出结构体变量名来达到输出结构体变量所有成员的值。 + +下面用法不正确: + +```c +printf("%s\n",b);//企图用结构体变量名输出所有成员的值 +``` + +只能对结构体变量中的各个成员分别进行输人和输出。 + +(3)如果成员本身又属一个结构体类型,则要用若干个成员运算符,一级一级地找到最低的一级的成员。只能对最低级的成员进行赋值或存取以及运算。如果在结构体`struct Student`类型的成员中包含另一个结构体`struct date`类型的成员`birthday`(为一个结构体) ,则引用成员的方式为: + +```c +studentl.num //(结构体变量studentl中的成员num) +studentl.birthday.month //(结构体变量studentl中的成员birthday中的成员month) +``` + +不能用`student1. birthday`来访问`student1` 变量中的成员`birthday`, 因为`birthday` 本身是一个结构体成员。 + +(4)对结构体变量的成员可以像普通变量一样进行各种运算(根据其类型决定可以进行的运算)。 + +例如: + +```c +student2.score = studentl.score; //(赋值运算) +sum = student1.score+student2.score; //(加法运算) +studentl.age++; //(自加运算) +``` + +由于`“.”`运算符的优先级最高,因此`studentl.age++`是对(`student1.age`)进行自加运算,而不是先对`age`进行自加运算。 + +(5)同类的结构体变量可以互相赋值,如: + +```c +studentl = student2;//假设student1和student2已定义为同类型的结构体变量 +``` + +(6)可以引用结构体变量成员的地址,也可以引用结构体变量的地址。 + +例如: + +```c +scanf("%d",&student1.num); //(输人studentl. num的值) +printf("%o",&student1); //(输出结构体变量studentl的起始地址) +``` + +但不能用以下语句整体读人结构体变量,例如: + +```c +scanf("%d,%s,%c,%d,%f,%s\n",&.studentl); +``` + +说明:结构体变量的地址主要用作函数参数,传递结构体变量的地址。 + +## 2. 使用结构体数组 + +(1)定义结构体数组一般形式是: + +```c +struct结构体名{ + 成员表列 +}数组名[数组长度]; +``` + +先声明一个结构体类型(如`struct Person`),然后再用此类型定义结构体数组: + +```c +结构体类型 数组名[数组长度]; + +struct Person{ + char name[20]; + int age; +}; + +struct Person leader[3]; //leader是结构体数组名 +``` + +(2)对结构体数组初始化的形式是在定义数组的后面加上: + +```c +结构体类型 数组名[数组长度]= {初值表列}; +``` + +如: + +```c +struct Person leader[3]= {"Li",0,"Zhang",0,"Sun",0}; +``` + +## 3. 结构体指针 + +**指向结构体变量的指针** + +指向结构体对象的指针变量既可指向结构体变量,也可指向结构体数组中的元素。指针变量的基类型必须与结构体变量的类型相同。 + +例如: + +```c +struct Student* pt; //pt可以指向structStudent类型的变量或数组元素 +``` + +说明:为了使用方便和直观,C语言允许把`(*p).num`用`p->num`代替,“`->`”代表 一个箭头,`p->num`表示`p`所指向的结构体变量中的`num`成员。同样,`(*p).name`等价于`p->name`。“`->`”称为指向运算符。 + +如果`p`指向一个结构体变量`stu`,以下3种用法等价: + +①`stu.成员名 (如stu. num);` + +②`(*p).成员名 (如(*p).num);` + +③`p->成员名 (如p->num)`。 + +**指向结构体数组的指针** + +可以用指针变量指向结构体数组的元素。 + +例如:有3个学生的信息,放在结构体变量中,要求输出全部学生的信息。 + +(1)声明结构体类型 `struct Student`,并定义结构体数组,同时初始化; + +(2)定义一个指向`struct Student` 类型数据的指针变量p; + +(3)使P指向结构体数组的首元素,输出它指向的元素中的有关信息; + +(4)使p指向结构体数组的下一个元素,输出它指向的元素中的有关信息; + +(5)再使p指向结构体数组的下一个元素,输出它指向的元素中的有关信息。 + +编写程序: + +```c +#include +struct Student{ //声明结构体类型structStudent + int num; + char name[20]; + char sex; + int age; +}; +struct Student stu[3]={{10101,"Li Lin",'M',18},{10102,"Zhang Fang",'M',19},{10104,"Wang Min",'F',20}}; //定 义结构体数组并初始化 +int main(){ + struct Student* p;//定义指向structStudent结构体变量的指针变量 + printf(" No. Name sex age\n"); + for (p= stu;pnum,p->name,p->sex,p->age);//输出结果 + } + return 0; +} +``` + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210516182619.png) + + + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210516182618.png) + + + +**注意:** + +(1)如果p的初值为`stu`,即指向`stu`的序号为0的元素,p加1后,就指向下一个元素。 + +例如: + +```c +(++p)->num //先使p自加1,然后得到p指向的元素中的num成员值(10102) +(p++)->num //先求得p->num的值(即10101),然后再使p自加1,指向stu[1] +``` + +请注意以上二者的不同。 + +(2)程序定义了p是一个指向`struct Student`类型对象的指针变量,它用来指向一个 `struct Student`类型的对象(p的值是`stu`数组的一个元素(如`stu[0]`或`stu[1]`)的起始地址),不应用来指向`stu`数组元素中的某一成员。 + +例如,下面的用法是不对的: + +```c +p= stu[1].name; //stu[1].name是stu[1]元素中的成员name 的首字符的地址 +``` + +编译时将给出“警告”信息,表示地址的类型不匹配。不要认为反正p是存放地址的,可以将任何地址赋给它。如果一定要将某一成员的地址赋给p,可以用强制类型转换,先将成员的地址转换成p的类型。例如: + +```c +p= (struct Student * )stu[0].name; +``` + +此时,p的值是`stu[0]`元素的name成员的起始地址。可以用“`printf("%s",p);`"输出 `stu[0]`中成员name的值。但是,p仍保持原来的类型。如果执行“`printf("%s',p+1);`,则 会输出`stu[1]`中name 的值。执行`p++`时,p的值的增量是结构体`struct Student`的长度。 + +**用结构体变量和结构体变量的指针作函数参数** + +将一个结构体变量的值传递给另一个函数,有3个方法: + +(1)用结构体变量的成员作参数。例如,用`stu[1]. num`或`stu[2].name`作函数实参,将实参值传给形参。用法和用普通变量作实参是一样的,属于“值传递”方式。应当注意实参与形参的类型保持一致。 + +(2)用结构体变量作实参。用结构体变量作实参时,采取的也是“值传递”的方式,将结构体变量所占的内存单元的内容全部按顺序传递给形参,形参也必须是同类型的结构体变量。在函数调用期间形参也要占用内存单元。这种传递方式在空间和时间上开销较大,如果结构体的规模很大时,开销是很可观的。此外,由于采用值传递方式,如果在执行被调用函数期间改变了形参(也是结构体变量)的值,该值不能返回主调函数,这往往造成使用上的不便。因此一般较少用这种方法。 + +(3)用指向结构体变量(或数组元素)的指针作实参,将结构体变量(或数组元素)的地址传给形参。 + +## 4. 用指针处理链表 + +**什么是链表?** + +链表是动态地进行存储分配的一种结构。 +作用是为了避免内存的浪费,它是根据需要开辟内存单元设定的。 + +**单向链表** +由 head 的 next 指向下个节点 +头指针:head (整个链表都必须包含head) +结点 :必须包含两部分(1)用户需要用的实际数据 (2)下一个节点的地址 +空指针(表尾):NULL + +**建立链表(利用结构体)** + +```c +struct Student +{ int num; + float score; + struct Student *next; //next是指针变量,指向下一个结构体的地址 +}; +``` + +**输出链表** + +```c +void output(struct student *head) // 定义一个链表输出的函数 +{ + struct student *p; // 定义结构体指针变量p1,用于结点的后移,以实现输出操作 + p = head; // 将head赋给p1,以实现对该链表的操作 + if (p != NULL) // 建立一个while循环,结束条件是到达尾结点 + do + { + printf("%d\n%f\n", p1->num, p1->score); // 输出结点中的数值部分 + p1 = p1->next; // 将下一个结点的位置赋给p1 + }while(p != NULL);//当p不是空地址时循环 + +} +``` + +**注意:** +`malloc()`分配内存后最后记得`free()`释放内存。 + +可以参看以下博客学习: + +https://blog.csdn.net/linwh8/article/details/49648601 + +******* + +## 5. 共同体类型 + +使几个不同的变量公享同一段内存的结构,称为"共同体"类型结构。 + +定义共同体类型变量的的一般形式为: + +```c +union 共用体名{ + 成员列表 +}变量列表; +``` + +例如: + +```c +union Data{ //表示不同类型的变量i,ch,f可以存放到同一段存储单元中 + int t; + char ch; + float f; +}a,b,c; //在声明类型同时定义变量 +``` + +也可以将类型声明与变量定义分开: + +```c +union Data{ //声明共用体类型 + int I; + char ch; + float f; +}; +union Data a,b,c; //用共用体类型定义变量 +``` + +即先声明一个`union Data`类型,再将a,b,c定义为`union Data`类型的变量。 + +当然也可以直接定义共用体变量,例如: + +```c +union{ //没有定义共用体类型名 + int i; + char ch; + float f; +}a,b,c; +``` + +可以看到,“共用体” 与 “结构体” 的定义形式相似。但它们的含义是不同的。 + +结构体变量所占内存长度是各成员占的内存长度之和。每个成员分别占有其自己的内存单元。而共用体变量所占的内存长度等于最长的成员的长度。例如,上面定义的“共用 体”变量a,b,c各占4个字节(因为一个float 型变量占4个字节),而不是各占4+1+4=9个字节。 + +**引用共用体变量的方式** + +只有先定义了共同体变量才能引用它,但应注意,不能引用共同变量,而只能引用共同体变量中的成员。 + +例如:【上面定义的a,b,c共用体】 + +```c +a.i //引用共同体变量中的整型变量i +a.ch //引用共同体变量中的整型变量i +a.f //引用共同体变量中的整型变量i + +//不能只引用共同体变量,下面的引用就是错误的 +printf("%d",a); + +//正确的写法为 +printf("%d",a.i); +printf("%c",a.ch); +printf("%f",a.f); +``` + +**** + +**共用体类型数据的特点** + +在使用共用体型数据时要主要以下特点: + +- 同一个内存段可以用来存放几种不同类型的成员,但在每一瞬时只能存放其中一个成员,而不是同时存放几个。 +- 可以对共用体变量初始化,但初始化表中只能有一个常量。 + - 以下用法为错误的: + +```c +union Data{ + int i; + char ch; + float f; +}a={1,'a',1.5}; //不能初始化3个成员变量,它们占用同一段存储单元 +union Data a={16}; //正确,对第1个成员初始化 +union Data a={.ch='j'}; //C99允许对指定的一个成员初始化 +``` + +- 共用体变量中起作用的成员是最后一次被赋值的成员,在对共用体变量中的一个成员赋值之后,原变量存储单元中的值就被取代了。 +- 共同体变量的地址和它的各成员的地址都是同一地址。 + - 例如: + +```c +&a.i,&a.ch,&a.f //都是同一地址 +``` + +- 不能对共同变量名赋值,也不能企图引用变量名来得到一个值。 + - 例如,下面这些都是不对的: + +```c +i = 1; //不能对共同体变量赋值,赋给谁? +m = a; //企图引用共同体变量名以得到一个值赋给整型变量m +************************************** +//C99允许同类型的共同体变量相互赋值。 +b = a; //a和b是同类型的共同变量,合法 +``` + +- 以前的C规定不能把共同体变量作为函数参数,但是可以使用指向共同体变量的指针作函数参数。`C99`允许用共同体变量作为函数参数。 +- 共同体类型可以出现在结构体类型定义中,也可以定义共用体数组。反之,结构体也可以出现在共用体类型中,数组也可以作为共用体的成员。 + +## 6. 使用枚举类型 + +如果一个变量只有几种可能的值,则可以定义为枚举(enumeration)类型,**所谓 “枚举” 就是指把可能的值一一列举出来,变量的值只限制于列举出来的值的范围内。** + +例如: + +```c +enum Weekday{sun,mon,tue,wed,thu,fri,sat}; +``` + +以上声明了一个枚举类型`enum Weekday`然后可以用此类型来定义变量。 + +例如: + +```c +enum Weekday workday,weekend; +----------- -------------- + | | + 枚举类型 枚举变量 +``` + +`workday`和`weekend`被定义为枚举变量,花括号中的`sun,mon,..,sat`称为枚举元素或枚举常量。它们是用户指定的名字。枚举变量和其他数值型量不同,它们的值只限于花括号中指定的值之一。例如枚举变量`workday`和`weekend`的值只能是`sun`到`sat`之一。 + +```c +workday= mon; //正确,mon是指定的枚举常量之一 + +weekend= sun;//正确,sun是指定的枚举常量之一 + +weekday = monday;//不正确,monday不是指定的枚举常量之一 +``` + +枚举常量是由程序设计者命名的,用什么名字代表什么含义,完全由程序员根据自己的需要而定,并在程序中作相应处理。 + +也可以不声明有名字的枚举类型,而直接定义枚举变量,例如: + +```c +enum{sun,mon,tue,wed,thu,fri,sat} workday,weekend; +``` + +**声明枚举类型用 `enum` 开头**。 + +声明枚举类型的一般形式为: + +```c +enum [枚举名]{枚举元素……}; +``` + +**说明:** + +(1) C编译对枚举类型的枚举元素按常量处理,故称枚举常量。不要因为它们是标识符(有名字)而把它们看作变量,不能对它们赋值。 + +(2) 每一个枚举元素都代表一个整数,C语言编译按定义时的顺序默认它们的值为`0,1,2,3,4,5...`。在上面的定义中,`sun`的值自动设为0,`mon`的值为1,.,`sat`的值为6。如果有赋值语句: + +```c +workday= mon;//相当于 +workday= 1;//枚举常量是可以引用和输出的。例如: +printf("%d",workday);//将输出整數1。 +``` + +也可以人为地指定枚举元素的数值,在定义枚举类型时显式地指定,例如: + +```c +enum Weekday{sun=7,mon=1,tue,wed,thu,fri,sat} workday,week_end; +``` + +指定枚举常量`sun`的值为7,`mon`为1,以后顺序加1,`sat`为6。 + +由于枚举型变量的值是整数,因此`C99`把枚举类型也作为整型数据中的一种,即用户自行定义的整数类型。 + +(3)枚举元素可以用来作判断比较。例如: + +```c +if( workday= = mon).. +if( workday> sun)... +``` + +枚举元素的比较规则是按其在初始化时指定的整数来进行比较的。如果定义时未人为 指定,则按上面的默认规则处理,即第1个枚举元素的值为0,故`mon > sun`,`sat > fri`。 + +## 7. 用typedef声明新类型名 + +用typedef指定新的类型名来代替已有的类型名。 + +有以下两种情况: + +1. **简单地用一个新的类型名代替原有的类型名** + +例如: + +```c +typedef int Integer; //指定用Integer为类型名,作用与int相同 +typedef float Real; //指定用Real为类型名,作用与float相同 +``` + +指定用`Integer`代表int类型,Real代表float。 这样,以下两行等价: + +```c +①int i,j; +float a,b; + +②Integer i,j; +Real a,b; +``` + +2. **命名一个简单的类型名代替复杂的类型表示方法** + +从前面已知,除了简单的类型(如int,float等)、C程序中还会用到许多看起来比较复杂的类型,包括结构体类型、共用体类型枚举类型、指针类型、数组类型等,如: + +```c +float*[](指针数组) +float( * )[5](指向5个元素的-维数组的指针) +double * (double * )(定义函数,函数的參数是double*型数据,即指向double数据的指针,函数返回值也是指向double数据的指针) +double( * )()(指向函数的指针,函数返回值类型为double) +int * ( * ( * )[10])(void)(指向包含10个元素的一维数组的指针,数组元素的类型为函数指针(函数的地址),函数没有参数,函数返回值是int 指针) +``` + +有些类型形式复杂,难以理解,容易写错。C允许程序设计者用一个简单的名字代替复杂的类型形式。 + +例如: + +(1)命名一个新的类型名代表结构体类型: + +```c +typedef struct int month; +int year; +} Date; +``` + +以上声明了一个新类型名Date,代表上面的一个结构体类型。然后可以用新的类型名Date去定义变量,如: + +``` c +Date birthday; //定义结构体类型变量birthday ,不要写成struct Date birthday; +Date* P; //定义结构体指针变量p.指向此结构体类型数据 +``` + +(2)命名一个新的类型名代表数组类型: + +```c +typedef int Num[ 100]; //声明Num为整型数组类型名 +Numa; //定义a为整型数组名,它有100个元素 +``` + +(3)命名一个新的类型名代表指针类型: + +```c +typedef char * String; //声明String为字符指针类型 +String p,s[10]; //定义p为字符指针变量,s为字符指针数组 +``` + +(4)命名一个新的类型名代表指向函数的指针类型: + +```c +typedefint(*Pointer)(); //声明Pointer为指向函数的指针类型,该函数返回整型值 +Pointer pl,p2; //p1,p2为Pointer类型的指针变量 +``` + +归纳起来,声明一个新的类型名的方法是: + +```txt +①先按定义变量的方法写出定义体(如:int i;)。 +②将变量名换成新类型名(例如:将i换成Count)。 +③在最前面加typedef(例如:typedef int Count)。 +④然后可以用新类型名去定义变量。 +``` + +简单地说,就是按定义变量的方式,把变量名换上新类型名,并且在最前面加typedef,就声明了新类型名代表原来的类型。 + +以定义上述的数组类型为例来说明: + +```txt +①先按定义数组变量形式书泻: int a[100]。 +②将变量名a换成自己命名的类型名:int Num[100]。 +③在前面加上typedef,得到typedef int Num[100]。 +④用来定义变量:Num a; +相当于定义了:int a[100]; +同样,对字符指针类型,也是: +①char * p; //定义变量p的方式 +②char * String; //用新类型名String 取代变量名p +③typedef char * String; //加typedef +④String p; //用新类型名String定义变量,相当char*p; +``` + +习惯上,常把用typedef声明的类型名的第1个字母用大写表示,以便与系统提供的标准类型标识符相区别。 + diff --git "a/docs/50.C\350\257\255\350\250\200/08.\347\254\254\344\270\203\347\253\240\345\257\271\346\226\207\344\273\266\347\232\204\350\276\223\345\205\245\350\276\223\345\207\272.md" "b/docs/50.C\350\257\255\350\250\200/08.\347\254\254\344\270\203\347\253\240\345\257\271\346\226\207\344\273\266\347\232\204\350\276\223\345\205\245\350\276\223\345\207\272.md" new file mode 100644 index 00000000..087cbaf5 --- /dev/null +++ "b/docs/50.C\350\257\255\350\250\200/08.\347\254\254\344\270\203\347\253\240\345\257\271\346\226\207\344\273\266\347\232\204\350\276\223\345\205\245\350\276\223\345\207\272.md" @@ -0,0 +1,253 @@ +--- +title: 第七章 对文件的输入输出 +date: 2021-05-11 14:30:00 +permalink: /c/note7/ +author: + name: eric + href: https://wfmiss.cn +--- +# 第七章 对文件的输入输出 + +## 1. C文件的有关基本知识 + +**什么是文件** + +文件有不同的类型,在程序设计中,主要用到两种文件: + +(1)程序文件。包括源程序文件(`后缀为.c`)、目标文件(`后缀为.obj`)、可执行文件(`后缀为.exe`)等。这种文件的内容是程序代码。 + +(2)数据文件。文件的内容不是程序,而是供程序运行时读写的数据,如在程序运行过程中输出到磁盘(或其他外部设备)的数据,或在程序运行过程中供读人的数据。如一批学生的成绩数据、货物交易的数据等。 + +- 文件(file)是程序设计中一个重要的概念。所谓“文件”一般指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质(如磁盘)上的。 + +- 输人输出是数据传送的过程,数据如流水一样从一处流向另一处,因此常将输人输出形象地称为流(stream) ,即**数据流**。 + +- C语言把文件看作一个字符(或字节)的序列,即由一个一个字符(或字节)的数据顺序组成。一个输入输出流就是一个字符流或字节(内容为二进制数据)流。 + +- C的**数据文件**由一连串的字符(或字节)组成,而不考虑行的界限,两行数据间不会自动加分隔符,对文件的存取是以字符(字节)为单位的。**输人输出数据流的开始和结束仅受程序控制而不受物理符号(如回车换行符)控制,**这就增加了处理的灵活性。这种文件称为**流式文件**。 + +******** + +**文件名** + +一个文件要有一个唯一的文件标识,以便用户识别和引用。 + +文件标识包括3部分: (1)文件路径; (2)文件名主干; (3)文件后缀。 + +文件路径表示文件在外部存储设备中的位置。如: + +```txt +D:\CC\temp\filel.dat +---------- ----- ------ + ↑ ↑ ↑ +文件路径 文件名主干 文件后缀 +``` + +表示`filel.dat `文件存放在`D`盘中的`CC`目录下的`temp`子目录下面。 + + 文件名主干的命名规则遵循标识符的命名规则。 + +后缀 用来表示文件的性质,如: + +```txt +doc (Word生成的文件) +txt (文本文件) +dat (数据文件) +c (C语 言源程序文件) +cpp (C++源程序文件) +for (FORTRAN语言源程序文件) +pas (Pascal语 言源程序文件) +obj (目标文件) +exe (可执行文件) +ppt (电子幻灯文件) +bmp (图形文件) +... +``` + +***** + +**文件的分类** + +根据数据的组织形式,数据文件可分为**ASCII文件**和**二进制文件**。数据在内存中是以二进制形式存储的,如果不加转换地输出到外存,就是二进制文件,可以认为它就是存储在内存的数据的映像,所以也称之为映像文件(`imagefile`)。如果要求在外存上以ASCII代码形式存储,则需要在存储前进行转换。ASCII文件又称文本文件(`text file`),每一个字节存放一个字符的ASCII代码。 + +**一个数据在磁盘上怎样存储呢?** + +**字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储。** + + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210518210127.png) + +***** + + +**文件缓冲区** + +ANSI C标准采用“缓冲文件系统”处理数据文件,所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区。从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。如果从磁盘向计算机读人数据,则一次从磁盘文件将一批数据输人到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量),见图10.2。这样做是为了节省存取时间,提高效率,缓冲区的大小由各个具体的C编译系统确定。 + +说明:每一个文件在内存中只有一个缓冲区,在向文件输出数据时,它就作为输出缓冲区,在从文件输入数据时,它就作为输入缓冲区。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210519085511.png) + +**文件类型指针** + +缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的有关信息(如文件的名字、文件状态及文件当前位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名为FILE。例如有一种C编译环境提供的`stdio.h`头文仵中有以下的文件类型声明: + +```c +typedef struct{ + short level; //缓冲区“满”或“空”的程度 + unsigned flags; //文件状态标志 + char fd; //文件描述符 + unsigned char hold; //如缓冲区无内容不读取字符 + short bsize; //缓冲区的大小 + unsigned char * buffer; //数据缓冲区的位置 + unsigned char * curp; //文件位置标记指针当前的指向 + unsigned istemp; //临时文件指示器 + short token; //用于有效性检查 +}FILE; +``` + +不同的C编译系统的FILE类型包含的内容不完全相同,但大同小异。对以上结构体中的成员及其含义可不深究,只须知道其中存放文件的有关信息即可。 + +定义一个指向文件型数据的指针变量: + +```c +FILE * fp; +``` + +定义`fp`是一个指向FILE类型数据的指针变量。可以使`fp`指向某一个文件的文件信息区(是一个结构体变量),通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。如果有n个文件,应设n个指针变量,分别指向n个FILE类型变量,以实现对n个文件的访问,见图10. 3。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/C_language/20210519091314.png) + +为方便起见,通常将这种指向文件信息区的指针变量简称为**指向文件的指针变量**。 + +注意:指向文件的指针变量并不是指向外部介质上的数据文件的开头,而是指向内存中的文件信息区的开头。 + +## 2. 打开与关闭文件 + +实际上,所谓 “打开” 是指为文件建立相应的信息区(用来存放有关文件的信息)和文件缓冲区(用来暂时存放输入输出的数据)。 + +在编写程序时,在打开文件的同时,一般都指定一个指针变量指向该文件,也就是建立起指针变量与文件之间的联系,这样,就可以通过该指针变量对文件进行读写了。所谓“关闭”是指撤销文件信息区和文件缓冲区,使文件指针变量不再指向该文件,显然就无法进行对文件的读写了。 + +**用`fopen`函数打开数据文件** + +`ANSIC`规定了用标准输人输出函数`fopen`来实现打开文件。 + +`fopen`函数的调用方式为: + +```c +fopen(文件名,使用文件方式); +``` + +例如: + +```c +fopen("al","r"); +``` + +**表示要打开名字为`al`的文件,使用文件方式为 “读入”(r代表read,即读人)**。 + +`fopen` 函数的返回值是指向`al`文件的指针(即`al`文件信息区的起始地址)。 + +通常将`fopen`函数的返回值赋给一个指向文件的指针变量。如: + +```c +FILE* fp; //定义一个指向文件的指针变量fp +fp= fopen("al","r"); //将fopen函数的返回值赋给指针变量fp +``` + +这样`fp`就和文件`al`相联系了,或者说,`fp`指向了`al`文件。可以看出,在打开一个文件时,通知编译系统以下3个信息: + +①需要打开文件的名字,也就是准备访问的文件的名字; + +②使用文件的方式(“读”还是“写”等); + +③让哪一个指针变量指向被打开的文件。 + +**表10. 1 使用文件方式** + +| 文件使用方式 | 含义 | 如果指定的文件不存在 | +| ------------- | -------------------------------------- | -------------------- | +| r(只读) | 为了输入数据,打开一个已存在的文本文件 | 出错 | +| w(只写) | 为了输出数据,打开一个文本文件 | 建立新文件 | +| a(追加) | 向文本文件尾添加数据 | 出错 | +| rb(只读) | 为了输人数据,打开一个二进制文件 | 出错 | +| wb(只写) | 为了输出数据,打开一个二进制文件 | 建立新文件 | +| ab(追加) | 向二进制文件尾添加数据 | 出错 | +| "r+"(读写) | 为了读和写,打开一个文本文件 | 出错 | +| "w+"(读写) | 为了读和写,建立一个新的文本文件 | 建立新文件 | +| "a+"(读写) | 为了读和写,打开一个文本文件 | 出错 | +| "rb+"(读写) | 为了读和写,打开一个二进制文件 | 出错 | +| "wb+"(读写) | 为了读和写,建立一个新的二进制文件 | 建立新文件 | +| "ab+"(读写) | 为读写打开一个二进制文件 | 出错 | + +(1)用 r 方式打开的文件只能用于向计算机输入而不能用作向该文件输出数据,而且该文件应该已经存在,并存有数据,这样程序才能从文件中读数据。不能用 r 方式打开一个并不存在的文件,否则出错。 + +(2)用 w 方式打开的文件只能用于向该文件写数据(即输出文件),而不能用来向计算机输入。如果原来不存在该文件,则在打开文件前新建立一个以指定的名字命名的文件。如果原来已存在一个以该文件名命名的文件,则在打开文件前先将该文件删去,然后重新建立一个新文件。 + +(3)如果希望向文件末尾添加新的数据(不希望删除原有数据),则应该用a方式打开。但此时应保证该文件已存在;否则将得到出错信息。打开文件时,文件读写位置标记移到文件末尾。 + +(4)用“r十”、“w+”、“a+”方式打开的文件既可用来输人数据,也可用来输出数据。用 “r+” 方式时该文件应该已经存在,以便计算机从中读数据。用 “w十” 方式则新建立一个文件,先向此文件写数据,然后可以读此文件中的数据。用 “a+” 方式打开的文件,原来的文件不被删去,文件读写位置标记移到文件末尾,可以添加,也可以读。 + +(5)如果不能实现 “打开” 的任务,fopen函数将会带回一个出错信息。出错的原因可能是:用 r 方式打开一个并不存在的文件;磁盘出故障;磁盘已满无法建立新文件等。此时fopen函数将带回一个空指针值`NULL`(在`stdio.h`头文件中,`NULL`已被定义为0)。 + +常用下面的方法打开一个文件: + +```c +if ((fp= fopen("filel","r"))== NULL){ + printf("cannot open this file\n"); + exit(0); +} +``` + +即先检查打开文件的操作有否出错,如果有错就在终端上输出cannot open this file。 + +`exit`函数的作用是关闭所有文件,终止正在执行的程序,待用户检查出错误,修改后重新运行。 + +(7)在表10.1中,有12种文件使用方式,其中有6种是在第一个字母后面加了字母b 的(如rb,wb,ab,rb+ ,wb+ ,ab+),b表示二进制方式。其实,带b和不带b只有一个区别,即对换行的处理。由于在C语言用一个`'\n'`即可实现换行,而在Windows系统中为实现换行必须要用“回车”和“换行”两个字符,即`'\r'`和`'\n'`。因此,如果使用的是文本文件并且用w方式打开,在向文件输出时,遇到换行符`'\n'`时,系统就把它转换为`'\r'`和'`'\n'`两个字符,否则在Windows系统中查看文件时,各行连成一片,无法阅读。同样,如果有文本文件且用r方式打开,从文件读人时,遇到`'\r'`和`'\n'`两个连续的字符,就把它们转换为`'\n'`一个字符。如果使用的是二进制文件,在向文件读写时,不需要这种转换。加b表示使用的是二进制文件,系统就不进行转换。 + +(8)如果用wb的文件使用方式,并不意味着在文件输出时把内存中按ASCII形式保存的数据自动转换成二进制形式存储。输出的数据形式是由程序中采用什么读写语句决定的。例如,用fscanf和fprintf函数是按ASCII方式进行输人输出,而fread和fwrite函数是 按二进制进行输人输出。各种对文件的输人输出语句,详见下一节(3.顺序读写数据文件)。 + +在打开一个输出文件时,是选w还是wb方式,完全根据需要,如果需要对回车符进行转换的,就用w,如果不需要转换的,就用wb。带b只是通知编译系统:不必进行回车符的转换。如果是文本文件(例如一篇文章),显然需要转换,应该用w方式。如果是用二进制形式保存的一批数据,并不准备供人阅读,只是为了保存数据,就不必进行上述转换。可以用wb方式。一般情况下,带b的用于二进制文件,常称为二进制方式,不带b的用于文本文件,常称为文本方式,从理论上说,文本文件也可以wb方式打开,但无必要。 + +(9)程序中可以使用3个标准的流文件——标准输入流、标准输出流和标准出错输出流。系统已对这3个文件指定了与终端的对应关系。标准输人流是从终端的输人,标准输出流是向终端的输出,标准出错输出流是当程序出错时将出错信息发送到终端。 + +程序开始运行时系统自动打开这3个标准流文件。因此,程序编写者不需要在程序中用fopen函数打开它们。所以以前我们用到的从终端输人或输出到终端都不需要打开终端 文件。系统定义了3个文件指针变量stdin,stdout和stderr,分别指向标准输人流、标准输出流和标准出错输出流,可以通过这3个指针变量对以上3种流进行操作,它们都以终端作为输人输出对象。例如程序中指定要从stdin所指的文件输人数据,就是指从终端键盘输人数据。 + +****** + +**用fcolse函数关闭数据文件** + +在使用完一个文件后应该关闭它,以防止它再被误用。“关闭”就是撤销文件信息区和文件缓冲区,使文件指针变量不再指向该文件,也就是文件指针变量与文件‘‘脱钩”,此后不能再通过该指针对原来与其相联系的文件进行读写操作,除非再次打开,使该指针变量重新指向该文件。 + +关闭文件用fclose函数。fclose函数调用的一般形式为: + +```c +fclose(文件指针); +``` + +如果不关闭文件就结束程序运行将会丢失数据。因为,在向文件写数据时,是先将数据输出到缓冲区,待缓冲区充满后才正式输出给文件。如果当数据未充满缓冲区时程序结束运行,就有可能使缓冲区中的数据丢失。用fclose 函数关闭文件时,先把缓冲区中的数据输出到磁盘文件,然后才撤销文件信息区。有的编译系统在程序结束前会自动先将缓冲区中的数据写到文件,从而避免了这个问题,但还是应当养成在程序终止之前关闭所有文件的习惯。 + +fclose函数也带回一个值,当成功地执行了关闭操作,则返回值为0;否则返回EOF(-1)。 + +## 3. 顺序读写数据文件 + +在顺序写时,先写入的数据存放在文件的中前面的位置,后写入的数据存放在文件中后面的数据。 + +在顺序读时,先读文件中最前面的数据,后读文件中后面的数据。 + +顺序读写需要用函数库来实现。使用前需要导入`#include` + +**怎么向文件读写字符** + +读写一个字符的函数 + +| 函数名 | 调用形式 | 功能 | 返回值 | +| ------- | -------------- | ---------------------------------------- | ----------------------------------------------------------- | +| `fgtc` | `fgetc(fp)` | 从`fp`指向的文件中读入一个字符 | 读成功,带回所读的字符,失败则返回文件结束标志EOF(即-1) | +| `fputc` | `fputc(ch,fp)` | 把字符ch写文件指针变量`fp`所指向的文件中 | 输入成功,返回值就是输出的字符;输出失败,则返回EOF(即-1) | + +说明:fgetc 的第1个字母f代表文件(file),中间的get表示“获取”,最后一个字母c表示字符(character),fgetc的含义很清楚:从文件读取一个字符。fputc也类似。 + + + +**此节未完,将于2022年6月后继续更新……** diff --git "a/docs/51.Git/01.Git\347\256\200\345\215\225\346\217\220\344\272\244.md" "b/docs/51.Git/01.Git\347\256\200\345\215\225\346\217\220\344\272\244.md" new file mode 100644 index 00000000..9efc509b --- /dev/null +++ "b/docs/51.Git/01.Git\347\256\200\345\215\225\346\217\220\344\272\244.md" @@ -0,0 +1,129 @@ +--- +title: Git - 简单提交 +date: 2021-05-15 08:36:23 +permalink: /git/simple-commit/ +--- + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [初始](#%E5%88%9D%E5%A7%8B) + - [安装Git](#%E5%AE%89%E8%A3%85git) + - [设置用户签名](#%E8%AE%BE%E7%BD%AE%E7%94%A8%E6%88%B7%E7%AD%BE%E5%90%8D) +- [提交到远程仓库](#%E6%8F%90%E4%BA%A4%E5%88%B0%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93) +- [教程](#%E6%95%99%E7%A8%8B) + + +## 初始 + +### 安装Git + +官网: https://git-scm.com/ + +下载慢可使用淘宝镜像下载:https://npm.taobao.org/mirrors/git-for-windows/ + +下载完之后安装,一键默认即可 + + + +### 设置用户签名 + +Git 首次安装必须设置一下用户签名,否则无法提交代码 + +基本语法: + +```sh +git config --global user.name 用户名 +git config --global user.email 邮箱 +``` + +签名作用: + +每次提交到远程时区分不同操作者,在每次的提交信息中能看到。 + +在 `C:\Users\用户名\`目录下 `.gitconfig` 文件查看配置信息 + +## 提交到远程仓库 + +1. 创建远程仓库 + + 在 GitHub/Gitee 中创建仓库 + + 主分支一般设置为 `master` + +2. 初始化本地库 + + 在本地仓库目录下,打开 Git Bash + + ```sh + git init + ``` + 项目文件夹下就会生成.git文件,这是一个隐藏文件。 + +3. 添加远程仓库地址 + + ```sh + git remote add origin https://github.com/oddfar/docs.git + ``` + + 把链接替换成自己的 + +4. 拉取远程仓库 + + ```sh + git pull origin master + ``` + 作用是拉取远程仓库的文件,拉取本地没有的文件和新更改的文件 + +5. 添加暂存区 + + ```sh + git add . + ``` + + ` .`把所有文件添加到暂存区 + + 添加指定文件:`git add 文件名` + + 注意 add 后面有空格 + +6. 提交本地库 + + ```sh + git commit -m "日志信息" + ``` + + 将暂存区的文件提交到本地库 + + 使用 `git status` 查看状态 + +7. 同步到远程 + + ```sh + git push origin master + ``` + + + + +至此,已成功提交到远程。 + +--- + +也可以创建好远程仓库后,直接克隆到本地 + +```sh +git clone https://github.com/oddfar/docs.git +``` + +把本地代码,复制到下载的目录 + +再从第五步开始提交到 GitHub + +https 在国内不太稳定,有时候链接不上,建议用 SSH 链接来对仓库进行管理 + +## 教程 + +新人推荐看尚硅谷的 Git 教程:[5h打通Git全套教程丨2021最新IDEA版(涵盖GitHub\Gitee码云\GitLab)](https://www.bilibili.com/video/BV1vy4y1s7k6) + diff --git "a/docs/51.Git/02.SSH\345\205\215\345\257\206\347\231\273\345\275\225.md" "b/docs/51.Git/02.SSH\345\205\215\345\257\206\347\231\273\345\275\225.md" new file mode 100644 index 00000000..5cccd101 --- /dev/null +++ "b/docs/51.Git/02.SSH\345\205\215\345\257\206\347\231\273\345\275\225.md" @@ -0,0 +1,88 @@ +--- +title: Git - SSH免密登录 +permalink: /git/SSH/ +date: 2021-05-20 13:05:16 +--- + + + + + +- [步骤](#%E6%AD%A5%E9%AA%A4) +- [好处](#%E5%A5%BD%E5%A4%84) +- [别名](#%E5%88%AB%E5%90%8D) + + + +## 步骤 + +我们可以看到远程仓库中还有一个 SSH 的地址,因此我们也可以使用 SSH 实现免密码登录! + +进入 C:\Users\Administrator\.ssh 目录生成公钥 + +```sh +ssh-keygen -t rsa +``` + +执行后会生成两个文件 + +复制 `id_rsa.pub` 文件内容 + +Gitee:设置 ->SSH公钥 + +GitHub:点击用户头像→Settings→SSH and GPG keys + +这俩平台 ssh 可设置同一个 + +## 好处 + +GitHub 服务器在国外,我们用 https 对仓库进行拉取、提交有时会链接不上,导致失败。 + +![image-20210520131847003](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210520131856.png) + +这时我们可以用 ssh 对项目就行管理 + +以 Gitee 为例: + +![image-20210520132020687](https://cdn.jsdelivr.net/gh/oddfar/static/img/20210520132022.png) + +## 别名 + +对于用 https 已经克隆在本地的仓库,我们可以加个“别名”来进行 SSH 链接 + +**1)基本语法** + +`git remote -v ` 查看当前所有远程地址别名 + +`git remote add` 别名 远程地址 + +**2)案例** + +我们提交到远程仓库的指令是: + +```sh +git push 远程仓库地址 分支 +``` + +这个地址可以是 https 也可以是 ssh + +但由于地址太长了,可以用“别名”代替地址! + +添加别名: + +```sh +git remote add ssh git@github.com:oddfar/docs.git +``` + +推送到远程仓库: + +```sh +git push ssh master +``` + + + +---- + +我们 clone 项目,默认有个“别名” `origin` 指向我们 clone 时的远程仓库地址(https或ssh...) + diff --git "a/docs/51.Git/05.Git\345\210\240\351\231\244\346\217\220\344\272\244\350\256\260\345\275\225.md" "b/docs/51.Git/05.Git\345\210\240\351\231\244\346\217\220\344\272\244\350\256\260\345\275\225.md" new file mode 100644 index 00000000..52b63dca --- /dev/null +++ "b/docs/51.Git/05.Git\345\210\240\351\231\244\346\217\220\344\272\244\350\256\260\345\275\225.md" @@ -0,0 +1,96 @@ +--- +title: Git - 删除提交记录 +date: 2021-05-15 08:39:07 +permalink: /git/delete-commit/ +--- + + + + + +- [删除所有记录](#%E5%88%A0%E9%99%A4%E6%89%80%E6%9C%89%E8%AE%B0%E5%BD%95) +- [删除上次记录](#%E5%88%A0%E9%99%A4%E4%B8%8A%E6%AC%A1%E8%AE%B0%E5%BD%95) +- [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) + + + +## 删除所有记录 + +不小心把密码或其他敏感信息提交到git,想清空所有commit信息记录,就像形成一个全新的仓库,且代码不变。 + + + +1. 切换到新的分支 + + ```sh + git checkout --orphan latest_branch + ``` + + + +2. 缓存所有文件(除了.gitignore中声名排除的) + + ```sh + git add -A + ``` + + + +3. 提交跟踪过的文件 + + ```sh + git commit -am "commit message" + ``` + + + +4. 删除master分支 + + ```sh + git branch -D master + ``` + + + +5. 重命名当前分支为master + + ```sh + git branch -m master + ``` + + + +6. 提交到远程master分支 + + ```sh + git push -f origin master + ``` + + + +以上是删除所有提交记录,那么如何修改内容覆盖上次记录呢? + +## 删除上次记录 + +如你只是想修改上次提交的代码,做一次更完美的commit,可以这样 + +(1)`git reset commitId`,(注:不要带--hard)到上个版本 + +(2)`git stash`,暂存修改 + +(3)`git push --force`, 强制push,远程的最新的一次commit被删除 + +(4)`git stash pop`,释放暂存的修改,开始修改代码 + +(5)`git add .` -> `git commit -m "massage"` -> `git push` + + + + + + +## 参考资料 + +- https://my.oschina.net/18y/blog/3064211 +- https://segmentfault.com/q/1010000002898735 + diff --git "a/docs/51.Git/11.GitHub\346\217\220\351\200\237.md" "b/docs/51.Git/11.GitHub\346\217\220\351\200\237.md" new file mode 100644 index 00000000..e4cb7849 --- /dev/null +++ "b/docs/51.Git/11.GitHub\346\217\220\351\200\237.md" @@ -0,0 +1,56 @@ +--- +permalink: /git/increase-speed/ +title: GitHub - 提速 +date: 2021-05-20 13:48:11 +--- + + + + + +- [GitHub 镜像访问](#github-%E9%95%9C%E5%83%8F%E8%AE%BF%E9%97%AE) +- [GitHub 文件加速](#github-%E6%96%87%E4%BB%B6%E5%8A%A0%E9%80%9F) +- [GitHub 仓库快速下载](#github-%E4%BB%93%E5%BA%93%E5%BF%AB%E9%80%9F%E4%B8%8B%E8%BD%BD) +- [GitHub + Jsdelivr](#github--jsdelivr) + + + +## GitHub 镜像访问 + +两个最常用的镜像地址: + +- [https://github.com.cnpmjs.org](https://github.com.cnpmjs.org) +- [https://hub.fastgit.org](https://hub.fastgit.org) + +网站的内容跟 GitHub 是完整同步,可在网站访问浏览 + +不推荐登录账户。 + + + +## GitHub 文件加速 + +- [https://ghproxy.com/](https://ghproxy.com/) + +- [https://gh.api.99988866.xyz/](https://gh.api.99988866.xyz/) + + + +## GitHub 仓库快速下载 + +建议 Gitee 导入 GitHub 的仓库,用 Gitee 下载 + + + +## GitHub + Jsdelivr + +我们在 GitHub 上的图片不可访问,可使用 Jsdelivr 进行加速访问,不只是图片 + +简单使用: + +``` +https://cdn.jsdelivr.net/gh/用户名/仓库名/文件路径 +``` + +详情访问官网 + diff --git "a/docs/52.\346\261\207\347\274\226/01.\346\261\207\347\274\226.md" "b/docs/52.\346\261\207\347\274\226/01.\346\261\207\347\274\226.md" new file mode 100644 index 00000000..3eeca5ca --- /dev/null +++ "b/docs/52.\346\261\207\347\274\226/01.\346\261\207\347\274\226.md" @@ -0,0 +1,6 @@ +--- +title: 汇编 +date: 2021-05-19 22:23:31 +permalink: /assembly/note/ +--- + diff --git "a/docs/60.SMC&P/01.\345\211\215\350\250\200.md" "b/docs/60.SMC&P/01.\345\211\215\350\250\200.md" new file mode 100644 index 00000000..7b01e62a --- /dev/null +++ "b/docs/60.SMC&P/01.\345\211\215\350\250\200.md" @@ -0,0 +1,242 @@ +--- +title: 微型计算机原理及应用 +date: 2021-05-16 15:30:00 +permalink: /SMC&P/note/ +author: + name: eric + href: https://wfmiss.cn +--- +# 微型计算机原理及应用 + +## 绪论 + +**本笔记适用于河北专接本考试,个人理解仅供参考,不喜勿喷!!!** + +- 世界上第一台可以由程序控制的计算机称为电子数字积分器与计算器(electronic numerical integrator and calculator , `ENIAC`)。它是在1946年为了弹道设计的需要而由 美国宾夕法尼亚大学研制出来的。 + +- 第一代是电子管数字计算机,其发展年代大约为1946年——1958年。此时计算机的逻辑元件采用电子管,主存储器采用磁鼓、磁芯,外存储器已开始采用磁带,软件主要用机器语言来编制程序,后期逐步发展了汇编语言。当时主要用作科学计算。 + +- 第二代是晶体管计算机。其发展年代大致为1958年一1964年。计算机的逻辑元件为晶体管,主存储器仍用磁芯,外存储器已开始使用磁盘,软件已开始有很大的发展,出现了各种高级语言及编译程序。此时计算机的应用已发展至各种事务的数据处理,并开始用于工业控制。 + +- 第三代是集成电路计算机,其发展年代为1964年——1971年。此时的计算机,其逻辑元件已开始采用小规模和中规模的集成电路,即所谓`SSI`和`MSI`主存储器仍以磁芯为主。软件发展更快,已有分时操作系统。会话式的高级语言也已出现并有相当的发展。小型计算机也随着集成电路规模的增大而很快地发展起来。应用的范围也日益扩大,企事业管理与工业控制都逐步引入小型计算机。 + +- 第四代是大规模集成电路发展起来之后的产物。这是从1971年之后发展起来的。 所谓大规模集成电路(`LSD`)是指在单片硅片,上可以集成1000至20000个晶体管的集成电路。由于`LSI`的体积小,耗能很少,可靠性很高,因而促使微型计算机以很快的速度在发展。 + +- 20世纪80年代以来,微型计算机的类型已很多,体积越来越小,功能越来越强。 + ++ 微型计算机(`microcomputer`)的特点,与大、中、小型计算机的区别,就在于其中央处理器(CPU)是集中在一小块硅片上的,而大、中、小型计算机的CPU则是由相当多的电路(或集成电路)组成的。为了区别于大、中、小型计算机的CPU,而称微型计算机的 CPU芯片为微处理器( `microprocessing unit`或`microprocesser` , `MPU`)。 + +微型计算机除有MPU作为中央处理器之外,还有以大规模集成电路制成的主存储器和输人输出接口电路。这三者之间是采用总线结构联系起来的。 + +如果再配上相应的外围设备如显示器(CRT)、键盘及打印机等,这就成为微型计算机系统(`microcomputersystem`)。实际上作为数据处理的必须是较完备的微型计算机系统。作为工业控制,尤其是小型仪器仪表或小型设备的检测控制,则可只用微型计算机、单板计算机或单片计算机,甚至是一位计算机,这样可以尽量缩小机器的体积,不过此时又得增加相应的检测通道和控制通道,如放大器和A/D或D/A转换器之类的辅助元件或电路。 + +## 1.计算机系统的组成 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/7.png) + +**微处理器:** + +微处理器简称`CPU`,是计算机的核心。 + +主要包括: + +- 运算器(算术逻辑运算器ALU) +- 控制器 +- 寄存器组 + + + +**存储器:** + +定义:计算机中的记忆装置。用于存放计算机工作过程中需要操作的数据和程序。 + +```txt +存储器: + - 内存储器 + + 随机存取储存储器(RAM) + + 只读存储器(ROM) + - 外存储器 + + 联机外存---硬磁盘 + + 脱机外存---各种移动存储设备 +``` + +内存储器特点: + +- 存取速度较快,容量相对较小。 +- 内存按单元组织,每一个单元都对应一个唯一的地址。 +- 每一个存储单元中存放`1Byte`数据。 +- 内存单元个数称为内存容量。 + +有关存储器的属于: + +- 存储容量 + - 存放的数据量。用字节表示。 +- 对存储器的操作 + - 读(出),写(入) + - 读:将内存单元的内容取入CPU,原单元内容不改变; + - 写:CPU将信息放入内存单元,单元中原来的内容被覆盖。 + + + +**接口:** + +口是CPU与外部设备间的桥梁。 + +主要功能: + +- 数据缓冲寄存器; +- 信号电平或类型的装换; +- 实现主机与外部设间的运行匹配。 + + + +**总线:** + +- 是一组导线和相关的控制、驱动电路的集合。 +- 是计算机系统个部件之间传输地址、数据和控制信息的通路 + +总线分为:地址总线(AB)、数据总线(DB)、控制总线(CB) + + + +**软件系统:** + +软件:为运行、管理和维护计算机系统或为实现某一功能而编写的各种程序的总和及其相关资料。 + +```txt +软件: + 系统软件 + 操作系统 + 编译系统 + 网络系统 + 工具软件 + 应用软件 + 用户软件 + 娱乐软件 + 工作软件 + ... +``` + + + +***** + +**主机系统特征** + +- 能够与CPU直接进行信息换的部件属于**主机系统** +- 不能够与CPU直接进行信息换的部件属于**外部设备** + +## 2. 微型计算机的一般工作过程 + +计算机的工作就是执行程序 + +**程序是指令的序列** + +计算机的工作就是按照一定的顺序,一条条地执行指令 + +**指令:**由人向计算机发出的,能够被计算计所识别的命令 + +计算机的工作是逐条执行由命令构成的程序 + +- 程序是指令的序列 + - 计算机的工作过程就是执行指令的过程 + +指令的执行过程: + +```txt +取指令 -----> 分析指令 -------> 读取操作数 ------> 执行指令 ------> 存放结果 +``` + +- 顺序执行:一条指令执行完了再执行下一条指令。 + - 执行时间=取指令+分析指令+执行指令 + - 设:三个部分执行的时间均为x,则:执行n条指令时间为y为:y=3n*x +- 并行指令:同时执行两条或多条指令。 + - 仅第1条指令需要3x时间,之后每经过1x,就有一条指令执行结束 + - 执行时间:y=3x+(n-1)x + +****** + +并行:更高的效率,更高的复杂度 + +相对于顺序执行方式,指令并行执行的优势用加速比S表示: + +- S=顺序执行花费的时间/并行执行花费的时间 + +**微机系统主要技术指标** + +常用名词术语: + +- 位(bit或b):最小的信息单位,二进制的一位数。 + +- 字节(byte或B):是计算机的最小存储单元。 + + - ```txt + 1 byte = 8 bit,从00000000~11111111 + 可表示255个状态(数值); + 一般数字、字母、普通字符用一个字节就可表示,但汉字、特殊符号等需要两个或多个字节来表示。 + ``` + +- 字(word或w):是计算机进行数据交换、加工、存储的基本运算单位、一个字由一个或若干个字节构成,通常将组成一个字的位数叫作该字的字长。 + + - 字长越长,在相同时间内能传送更多的信息,从而运算速度更快;计算机有更大的寻址空间,从而内存储容量更大;计算机系统支持的指令数量越多,功能就越强。 + +**主要技术指标** + +- 机器字长:是指计算机内部ALU能够一次同时处理的二进制的位数。 + - 字长越长,运算精度越高。通常字长都是字节(8位二进制数)的整数倍,如8位、16位、32位、64位等。 +- 主频:计算机的时钟频率,在一定程度上反映机器运算速度,主频越高,运算速度越快。主频的单位是MHz(兆赫)。 + - 频率 = 1/周期 +- 内存容量:计算机可存储信息的多少,通常以字节为单位。一般用KB、MB、GB、TB、PB为单位。 + - 容量越大存储的程序和数据越多,处理能力也越强。【2的10次方=1024】 +- 运算速度:运算速度的一种表示方式是MIPS(millions of instructions per second),即每秒百万条指令,它主要是对整数运算而言。对于浮点运算,一般使用MFLOPS(million floating point operations per second)表示,即每秒百万次浮点运算。 + +***** + +**流水线技术** + +计算机中的流水线就是把一个重复的过程与其他子过程并行进行。 + +即: + +```txt +取指令 -----》分析指令 ------》执行指令 + 取指令 ------》分析指令 ------》执行指令 + 取指令 ------》分析指令 ------》执行指令 + ..... +``` + +从本质上讲,流水线技术就是一种时间并行技术。 + +## 3. 冯·诺依曼计算机 + +- 冯·诺依曼计算机的工作原理 + - 存储程序工作原理 +- 结构特点 + - 运算器为核心 + - 程序存储、共享数据、顺序执行 + - 属于顺序处理机,适合于确定的算法和数值数据的处理。 +- 不足: + - 与存储器间有大量数据交互,对总线要求很高; + - 执行顺序由程序决定,对大型复杂任务较困难; + - 以运算器为核心,处理效率较低; + - 由PC控制执行顺序,难以进行真正的并行处理。 + +******* + +## 4. 哈弗结构 + +- 指令和数据分别存放在两个独立的存储器模块中; + +- CPU与存储器间指令和数据的传送分别采用两组独立的总线; +- 可以在一个机器周期内同时获得指令操作码和操作数。 + +```txt +|-----------|----->(地址信号) |---------------| +| | | 程序存储器 | +| |<---- (数据信号) |---------------| +| CPU |================================== +| |------>(地址信号)|---------------| +| | | 数据存储器 | +|-----------| <-----(数据信号)|---------------| +``` + diff --git "a/docs/60.SMC&P/02.\347\254\254\344\270\200\347\253\240\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/60.SMC&P/02.\347\254\254\344\270\200\347\253\240\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200\347\237\245\350\257\206.md" new file mode 100644 index 00000000..ba8083e9 --- /dev/null +++ "b/docs/60.SMC&P/02.\347\254\254\344\270\200\347\253\240\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -0,0 +1,986 @@ +--- + +title: 第一章 计算机基础知识 +date: 2021-05-16 15:30:00 +permalink: /SMC&P/note/1 +author: + name: eric + href: https://wfmiss.cn +--- + +# 第一章 计算机基础知识 + +## 1. 数制 + +数制是人们用来记数的科学方法。如:十六进制、十进制、八进制、二进制 + +### 1.1数制的基与权 + +数制所使用的数码个数称为基;数制每一位所具有的值称为权。 + +例如 : + +- 二进制的基为 :2,即其使用的数码为 `0,1`,共两个。 +- 二进制各位的权是以2为低的幂,如下面这个数: + +```txt +二进制 :1 1 0 1 1 1 + (2^5)*1+(2^4)*1+(2^3)*0+(2^2)*1+(2^1)*1+(2^0)*1 +十进制 :32 16 8 4 2 1 +``` + +其各位的权为1,2,4,8,16,32,即以2为底的0次幂,1次幂,2次幂等,故有时也依次称其各位为0权位,1权位,2权位等。 + +其他进制同上。 + +**为什么要用二进制?** + +电路通常只有两种稳态:导通与阻塞、饱和与截止、高电位与低电位等。具有两个稳态的电路称为二值电路。因此,用二值电路来计数时,只能代表两个数码:0和1。如以1代表高电位,则0代表低电位,所以,采用二进制,可以利用电路进行计数工作。而用电路来组成计算机,则有运算迅速、电路简便、成本低廉等优点。 + +**为什么要用十六进制** + +用十六进制既可简化书写,又便于记忆。 + +有用字母符号来表示这些数制的: + +`B`——二进制:0,1 + +`O`——八进制:0,1,2,3,4,5,6,7 + +`D`—— 十进制:0,1,2,3,4,5,6,7,8,9 + +`H`——十六进制:0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F + +**数制的转换方法** + +**十进制数到非十进制数的转换** + +- 对二进制的转换 + - 对整数:除2取余 + - 对小数:乘2取整 +- 对八进制的转换 + - 对整数:除8取余 + - 对小数:乘8取整 +- 对十六进制的转换 + - 对整数:除16取整 + - 对小数:乘16取整 + +**非十进制数与二进制数的转换** + +- 十六进制数与二进制的转换 + - 用4为二进制数表示一位十六进制数 + - 整数部分,从小数点向左,每4位一组,不够4位的高位补0。 + - 小数部分,从小数点向右,每4位一组,不够4位的高位补0。 + +例: + +```txt +25.5D = 11001.1B +11001.1B = 0001 1001.1000B + ---- ---- ---- + 1 9 . 8 H +``` + +- 八进制数与二进制的转换 + - 用3为二进制数表示一位八进制数 + - 整数部分,从小数点向左,每4位一组,不够3位的高位补0。 + - 小数部分,从小数点向右,每4位一组,不够3位的高位补0。 + +例: + +```txt +11001010.0110101B=011 001 010.011 010 100B + --- --- --- --- --- --- + 3 1 2 . 3 2 4 O +``` +注意事项: +(1)一个二进制数可以准确地转换为十进制数,而一个带小数的十进制数不一定能够准确地用二进制数来表示。 +(2)带小数的十进制数在转换为二进制数时,以小数点为界,整数和小数要分别转换。 +***** + +### 1.2编码 + +- 信息从一种形式或格式转换为另一种形式的过程 +- 用代码来表示各种信息,以便计算机处理 + +**计算机中的编码** + +- 数值编码 + - 二进制编码 + - BCD编码 +- 西文字符编码 + - ASCII编码 + +**BCD编码:** + +- 用二进制表示十进制数 +- 特点:保留十进制的权,数字用0和1表示。 +- `8421BCD`编码: + - 用4为二进制码表示一位十进制数,每4位之间有一个空格 + - 注意:**1010~1111是非法BCD码,只是合法的十六进制数** +- BCD码与十进制数之间存在直接对应关系,例如: + +```txt +(1001 1000 0110.0011)BCD = 986.3 +----- ---- ---- ---- + | | | | + 9 8 6 . 3 +``` + +- BCD码与二进制的转换:先转换为十进制数,再转换为二进制数;反之同样。 +- 以压缩BCD码形式存放: + - 用4位二进制码表示1位BCD码 + - 一个存储单元中存放2位BCD数 +- 以扩展BCD码形式存放 + - 用8位二进制码表示1位BCD码,即高4位为0,第四位为有效位 + - 每个存储单元存放1位BCD + +**西文字符编码(ASCII编码)** + +将每个字母、数字、标点、控制符用1`Byte`二进制码表示,其中:**标准ASCII的有效位:`7Byte`,最高默认为0。** + +**ASCII 编码一览表** + +| 二进制 | 十进制 | 十六进制 | 字符/缩写 | 解释 | +| -------- | ------ | -------- | -------------------------------------------- | ---------------------------------- | +| 00000000 | 0 | 00 | NUL (NULL) | 空字符 | +| 00000001 | 1 | 01 | SOH (Start Of Headling) | 标题开始 | +| 00000010 | 2 | 02 | STX (Start Of Text) | 正文开始 | +| 00000011 | 3 | 03 | ETX (End Of Text) | 正文结束 | +| 00000100 | 4 | 04 | EOT (End Of Transmission) | 传输结束 | +| 00000101 | 5 | 05 | ENQ (Enquiry) | 请求 | +| 00000110 | 6 | 06 | ACK (Acknowledge) | 回应/响应/收到通知 | +| 00000111 | 7 | 07 | BEL (Bell) | 响铃 | +| 00001000 | 8 | 08 | BS (Backspace) | 退格 | +| 00001001 | 9 | 09 | HT (Horizontal Tab) | 水平制表符 | +| 00001010 | 10 | 0A | LF/NL(Line Feed/New Line) | 换行键 | +| 00001011 | 11 | 0B | VT (Vertical Tab) | 垂直制表符 | +| 00001100 | 12 | 0C | FF/NP (Form Feed/New Page) | 换页键 | +| 00001101 | 13 | 0D | CR (Carriage Return) | 回车键 | +| 00001110 | 14 | 0E | SO (Shift Out) | 不用切换 | +| 00001111 | 15 | 0F | SI (Shift In) | 启用切换 | +| 00010000 | 16 | 10 | DLE (Data Link Escape) | 数据链路转义 | +| 00010001 | 17 | 11 | DC1/XON (Device Control 1/Transmission On) | 设备控制1/传输开始 | +| 00010010 | 18 | 12 | DC2 (Device Control 2) | 设备控制2 | +| 00010011 | 19 | 13 | DC3/XOFF (Device Control 3/Transmission Off) | 设备控制3/传输中断 | +| 00010100 | 20 | 14 | DC4 (Device Control 4) | 设备控制4 | +| 00010101 | 21 | 15 | NAK (Negative Acknowledge) | 无响应/非正常响应/拒绝接收 | +| 00010110 | 22 | 16 | SYN (Synchronous Idle) | 同步空闲 | +| 00010111 | 23 | 17 | ETB (End of Transmission Block) | 传输块结束/块传输终止 | +| 00011000 | 24 | 18 | CAN (Cancel) | 取消 | +| 00011001 | 25 | 19 | EM (End of Medium) | 已到介质末端/介质存储已满/介质中断 | +| 00011010 | 26 | 1A | SUB (Substitute) | 替补/替换 | +| 00011011 | 27 | 1B | ESC (Escape) | 逃离/取消 | +| 00011100 | 28 | 1C | FS (File Separator) | 文件分割符 | +| 00011101 | 29 | 1D | GS (Group Separator) | 组分隔符/分组符 | +| 00011110 | 30 | 1E | RS (Record Separator) | 记录分离符 | +| 00011111 | 31 | 1F | US (Unit Separator) | 单元分隔符 | +| 00100000 | 32 | 20 | (Space) | 空格 | +| 00100001 | 33 | 21 | ! | | +| 00100010 | 34 | 22 | " | | +| 00100011 | 35 | 23 | # | | +| 00100100 | 36 | 24 | $ | | +| 00100101 | 37 | 25 | % | | +| 00100110 | 38 | 26 | & | | +| 00100111 | 39 | 27 | ' | | +| 00101000 | 40 | 28 | ( | | +| 00101001 | 41 | 29 | ) | | +| 00101010 | 42 | 2A | * | | +| 00101011 | 43 | 2B | + | | +| 00101100 | 44 | 2C | , | | +| 00101101 | 45 | 2D | - | | +| 00101110 | 46 | 2E | . | | +| 00101111 | 47 | 2F | / | | +| 00110000 | 48 | 30 | 0 | | +| 00110001 | 49 | 31 | 1 | | +| 00110010 | 50 | 32 | 2 | | +| 00110011 | 51 | 33 | 3 | | +| 00110100 | 52 | 34 | 4 | | +| 00110101 | 53 | 35 | 5 | | +| 00110110 | 54 | 36 | 6 | | +| 00110111 | 55 | 37 | 7 | | +| 00111000 | 56 | 38 | 8 | | +| 00111001 | 57 | 39 | 9 | | +| 00111010 | 58 | 3A | : | | +| 00111011 | 59 | 3B | ; | | +| 00111100 | 60 | 3C | < | | +| 00111101 | 61 | 3D | = | | +| 00111110 | 62 | 3E | > | | +| 00111111 | 63 | 3F | ? | | +| 01000000 | 64 | 40 | @ | | +| 01000001 | 65 | 41 | A | | +| 01000010 | 66 | 42 | B | | +| 01000011 | 67 | 43 | C | | +| 01000100 | 68 | 44 | D | | +| 01000101 | 69 | 45 | E | | +| 01000110 | 70 | 46 | F | | +| 01000111 | 71 | 47 | G | | +| 01001000 | 72 | 48 | H | | +| 01001001 | 73 | 49 | I | | +| 01001010 | 74 | 4A | J | | +| 01001011 | 75 | 4B | K | | +| 01001100 | 76 | 4C | L | | +| 01001101 | 77 | 4D | M | | +| 01001110 | 78 | 4E | N | | +| 01001111 | 79 | 4F | O | | +| 01010000 | 80 | 50 | P | | +| 01010001 | 81 | 51 | Q | | +| 01010010 | 82 | 52 | R | | +| 01010011 | 83 | 53 | S | | +| 01010100 | 84 | 54 | T | | +| 01010101 | 85 | 55 | U | | +| 01010110 | 86 | 56 | V | | +| 01010111 | 87 | 57 | W | | +| 01011000 | 88 | 58 | X | | +| 01011001 | 89 | 59 | Y | | +| 01011010 | 90 | 5A | Z | | +| 01011011 | 91 | 5B | [ | | +| 01011100 | 92 | 5C | \ | | +| 01011101 | 93 | 5D | ] | | +| 01011110 | 94 | 5E | ^ | | +| 01011111 | 95 | 5F | _ | | +| 01100000 | 96 | 60 | ` | | +| 01100001 | 97 | 61 | a | | +| 01100010 | 98 | 62 | b | | +| 01100011 | 99 | 63 | c | | +| 01100100 | 100 | 64 | d | | +| 01100101 | 101 | 65 | e | | +| 01100110 | 102 | 66 | f | | +| 01100111 | 103 | 67 | g | | +| 01101000 | 104 | 68 | h | | +| 01101001 | 105 | 69 | i | | +| 01101010 | 106 | 6A | j | | +| 01101011 | 107 | 6B | k | | +| 01101100 | 108 | 6C | l | | +| 01101101 | 109 | 6D | m | | +| 01101110 | 110 | 6E | n | | +| 01101111 | 111 | 6F | o | | +| 01110000 | 112 | 70 | p | | +| 01110001 | 113 | 71 | q | | +| 01110010 | 114 | 72 | r | | +| 01110011 | 115 | 73 | s | | +| 01110100 | 116 | 74 | t | | +| 01110101 | 117 | 75 | u | | +| 01110110 | 118 | 76 | v | | +| 01110111 | 119 | 77 | w | | +| 01111000 | 120 | 78 | x | | +| 01111001 | 121 | 79 | y | | +| 01111010 | 122 | 7A | z | | +| 01111011 | 123 | 7B | { | | +| 01111100 | 124 | 7C | \| | | +| 01111101 | 125 | 7D | } | | +| 01111110 | 126 | 7E | ~ | | +| 01111111 | 127 | 7F | DEL (Delete) | 删除 | + +**需要记住的:** + +- 0~9 十进制ASCII编码 48~57 +- A~Z 十进制ASCII编码 65~90 +- a~z 十进制ASCII编码 97~122 +- 大写字母与小写字母相差 32 + +***** + +### 1.3ASCII奇偶校验 + +- 奇校验 + - 加上校验位后编码中 “1” 的个数为奇数。 + - 例如:A的ASCII编码是41H(1000001B) + - 以奇校验传送则为C1H(11000001B) +- 偶校验 + - 加上校验位编码中 “1” 的个数为偶数。 + - 上例若以偶校验传送,则为41H。 + +***** + +### 1.4计算机中的二进制数表示 + +数的表示方法: + +- 定点数 + - 定点整数 + - 定点小数 +- 浮点数 + +定点数: + +- 编程时需要确定小数位置 +- 难以表示两个大小相差较大的数 +- 存储空间利用率低 + +浮点数: + +- 小数的位置可以左右移动 +- 规格化浮点数 + - 尾数部分用纯小数表示,即小数点右边第一位不为0 + +**** + +数的性质: + +- 无符号数 + - 数中所有0和1都是数据本身 + - 运算: + +```txt +加法: + 1 + 1 = 0(有进位) +减法: + 0 - 1 = 1(有错位) +乘法: + 00001011*0100 = 00101100B(向左移了两位) +除法: + 00001011/0100 = 00000010B(向右移动两位) + 商数:00000010B 余数:-11B +===================================================================== +每乘以2,相对于被乘数向左移动1位 +每除以2,相对于被除数向右移动1位 +``` + +- 有符号数 + - 需要0或1表示数的性质(整数或负数) + - 最高位表示符号,其余是数值 + - 符号数的表示方法: + +```txt +- 原码:最高位为符号位,其余为真值部分 +【X】原=符号位+|绝对值| +优点:真值和其源码表示之间的对应关系简单,容易理解; +缺点: +1. 计算机中用源码进行加减运算比较困难 +2. 0的表示不唯一 + +数0的原码: +8位数0的原码: +【+0】原= 0 0000000 +【-0】原= 1 0000000 +数0的原码不唯一 +************************************** +- 反码 +对于一个机器数x: +若x>0,则【x】反=【x】原 +若x<0,则【x】反=对应源码的符号位不变,数值部分按位求反。 +例: +x= -52 = -0110100 +【x】原= 10110100 +【x】反= 11001011 + +数0的反码: +【+0】反=【+0】原= 00000000 +【-0】原= 10000000 +【-0】反= 【-0】数值部分按位取反= 11111111 +即:数0的反码也不唯一的 +**************************************** +- 补码 +若x>0,则【x】补=【x】反=【x】原 +若x<0,则【x】补=【x】反+1 +例: +x= -52 = -0110100 +【x】原= 10110100 +【x】反= 11001011 +【x】补= 11001100 + +0的补码: +【+0】补=【+0】原= 00000000 +【-0】补=【-0】反+1= 11111111+1 = 1 00000000(对八位字长,进位被舍掉) +``` + +补码的算数运算 + +- 通过引进补码,可将减法运算转换位为加法运算。 +- 即: + +```txt +【x+y】补=【x】补+【y】补 +【x-y】补=【x+(-y)】补=【x】补+【-y】补 +``` + +**特殊数** + +- 对无符号数: + - (10000000)B = 128 +- 在原码中定义为: + - (10000000)B = -0 +- 在反码中定义为: + - (10000000)B = -127 +- 在补码中定义为: + - (10000000)B = -128 + +**数的性质由设计者决定** + +在低级语言程 + +序设计中,根据数的性质由程序语言处理(按无符号数或有符号数处理)。 + +***** + +### 1.5计算机能力的有限性 + +- 计算机的运行能力是有限的 + - 计算机无力解决无法设计出算法的问题 + - 无法处理无穷运算或连续变化的信息 +- 计算机能够表示的数(表数)的范围是有限的 + - 计算机的表数范围受字长的限制 + - 例如:对8位机 + - 无符号数的最大值:1111 1111 + - 有符号正整数的最大值:0111 1111 + +*************** + +当运算结果超出计算机表数范围时,将产生溢出 + +- 无符号整数的表示范围: + - 当计算机中数的运行结果超出表述范围时,则产生溢出。 + - 无符号整数的表数范围:`0 <= x <= [2^(n-1)]`,n表示字长 + - 无符号数加减运算溢出的判断方法: + - 当最高位向更高位有进位(或借位)时则产生溢出 +- 有符号整数的表示范围: + - 原码和反码:`[2^(n-1)]-1 <= x <= [2^(n-1)]-1` + - 补码:`[2^(n-1)] <= x <= [2^(n-1)]-1` + - 对于8位二进制数: + - 原码:-127~+127 + - 反码:-127~+127 + - 补码:-128~+127 + +- 符号数运算中的溢出判断 + - 两个符号数相加或相减时,若运算结果超出可表达范围,则产生溢出 + - 溢出的判断方法: + - 最高位进位状态和次高位进位状态不一样,则结果溢出 + - 除法运算溢出时,产生“除数为0”中断 + - 乘法运算无溢出问题 + +**** + +**符号二进制数与十进制的转换** + +转换方法: + +- 求出真值 +- 进行转换 + +计算机中的符号数默认以补码形式表示。 + +```txt +原码 = 符号位 + 绝对值 +正数的补码 = 原码 = 符号位 + 绝对值 +负数的补码 != 原码 != 符号位 + 绝对值 + +例如: +设:【x】补=0 010 1110B ----> 真值= +010 1110B ----> x = +101110B = +46 +设:【x】补=1 101 0010B ----> x != -101 0010B ----> 欲求x的真值,需对【x】补再取补码 + 【【x】补】补 = [11010010]补 = -010 1110 = -46 +``` + +- 对正数:补码 = 反码 = 原码,且 原码 = 符号位 + 真值 + - 所以:正数补码的数制部分为真值 +- 对负数:补码 != 反码 != 原码 + - 所以:负数补码的数制部分 != 真值 + +只有原码的数值部分是真值 + +反码和补码的数值部分都不是真值 + +## 2. 逻辑电路 + +逻辑电路由其3种基本门电路(或称判定元素)组成。图1-1是基本门电路的名称、符号及表达式。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210517160013.png) + +在这3个基本门电路的基础上,还可发展成如图1-2那样更复杂的逻辑电路。其中, 最后一个叫作缓冲器(buffer) ,为两个非门串联以达到改变输出电阻的目的。如果A点左边电路的输出电阻很高,则经过这个缓冲器之后,在Y点处的输出电阻就可以变得低许多倍,这样就能够提高带负载的能力。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210517160549.png) + +### 2.1”与非“逻辑 + +即”与“运算和”非“运算的一种组合 + +逻辑关系:!(A*B)或者!(A^B) + +将与门的输出接入非门的输入,构成”与非”门 + +### 2.2“或非”逻辑 + +即”或“运算和”非“运算的一种组合 + +逻辑关系:!(A+B)或者!(AvB) + +将与门的输出接入非门的输入,构成”与非”门 + +**”与非门“和“或非门”小结** + +- ”与非门“及“或非门”均为多输入单输出的门电路 + +- 可以实现多个变量的“与非”或者“或非”运算 + +练习: + +```txt +设:A=11001010,B=10101001 +计算: +1.Y= !(A*B) +Y = ![1100 1010 * 1010 1001]=![1000 1000]= 0111 0111 +2.Y= !(A+B) +Y = ![1100 1010 + 1010 1001]=![1110 1011]= 0001 0100 +``` + +### 2.3”异或“逻辑 + +“异或”逻辑关系是在与、或、非3种基本逻辑运算基础上的变换。 + +异或逻辑的布尔代数表达式:`F=(!A)*B+A*(!B)=A⊕B` + +“异或”运算是两个变量的运算 + +运算规则:相同则为0,相异则为1。 + +### 2.4”同或“逻辑 + +”同或“运算是在”异或“运算的基础上再进行”非“运算的结果。 + +同或运算的布尔表达式:`F=![(!A)*B+A*(!B)]=!(A⊕B)` + +**“同或”运算是两个变量的运算** + +运算规则:相同则为1,相异则为0。 + +## 3. 布尔代数 + +布尔代数也成为开关代数或逻辑代数,和一般线性代数一样,可以写成下面的表达式: + +```txt +Y = f(A,B,C,D) +``` + +但它有两个特点: + +- 其中的A,B,C,D等均只有两种可能的数值:0或1。布尔代数变量的数值并无大小之意,只代表事务的两个不同属性。如用于开关,则:0代表关(断路)或低电位;1代表开(通路)或高电位。如用于逻辑推理,则:0代表错误(假);1代表正确(真)。 +- 函数 f 只有3种基本方式:“或”运算、“与”运算、“反”运算。 + +*********** + +### 3.1“或”运算(OR) + +由于A,B只有0或1的可能取值,所以其各种可能结果如下: + +```txt +Y=0+0=0 → Y=0 + +Y=0+1=1 | +Y=1+0=1 |→ Y=1 +Y=1+1=1 | +``` + +上述第4个式子与一般的代数加法不符,这是因为Y也只能有两种数值:0或1。 + +面4个式子可归纳成两句话:两者皆伪者则结果必伪,有一为真者则结果必真。 + +这个结论也可推广至多变量:A,B,C,D...,各变量全假者则结果必假,有一为真者则结果必真。 + +在输入的“或”门电路中,只要其中有一个输入为1,则其输入必为1。或者说只有全部输入均为0时,输出才为0。 + +或运算有时也称为“逻辑或”。当A和B为多位二进制数时,如: + +```txt +A=A1A2A3....An +B=B1B2B3....Bn + +则进行“逻辑或”运算时,各对应位分别进行“或”运算: +Y =A+B +=(A1 + B1)(A2 + B2)(A3 + B3)...(An + Bn) +``` + +注意: 1 “或” 1等于1,是没有进位的。 + +例如: + +```txt +设 A =10101,B =11011,则Y=A+B +Y=(1 + 1)(0 + 1)(1 + 0)(0 + 1)(1 + 1) = 11111 +写成竖式则为: + 10101 ++ 11011 +-------- + 11111 +``` + +**总结:** + +- 输入条件中有一个为”真“,则输出的结果为”真“ +- 仅当输入条件全部为“假”时,输出结果才为“假” + +- ”或“运算符号:`+`、`v` +- 在电路中,”或“运算相当于开关的并联电路 + - 仅当所有开关都断开时,电路才无电流通过。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210523155148.png) + +*********** + +### 3.2“与”运算(AND) + +根据A和B的取值(0或1)可以写出下列各种可能的运算结果: + +```txt +Y=0*0=0| +Y=1*0=0|→ Y=0 +Y=0*1=0| + +Y=1*1=1 → Y=1 +``` + +这种运算结果也可归纳成两句话:二者为真者结果必真,有一为假者结果必假。 + +同样,这个结论也可推广至多变量:各变量均为真者结果必真,有一为假者结果必假。 + +在多输入“与”门电路中,只要其中一个输入为0,则输出必为0,或者说,只有全部输入均为1时,输出才为1。 + +与运算有时也称为“逻辑与”。当A和B为多位二进制数时,如: + +```txt +A=A1A2A3....An +B=B1B2B3....Bn +则进行“逻辑与”运算时,各对应位分别进行“与”运算: +Y=A * B = (A1 * Bl)(A2 * B2)(A3 * B3)...(An* Bn) + +例如:设A=11001010,B = 00001111,则Y = A * B +Y = (1 * 0)(1 * 0)(0 * 0)(0 * 0)(1 * 1)(0 * 1)(1 * 1)(0 * 1)= 00001010 +写成竖式则为: + 11001010 +* 00001111 +----------- + 00001010 +``` + +由此可见,用“0”和一个数位相“与”,就是将其‘‘抹掉”而成为“0”;用“1”和一个数位相“与”,就是将此数位“保存”下来。 + +这种方法在计算机的程序设计中经常会用到,称为“屏蔽”。 + +上面的B数(0000 1111)称为“屏蔽字”,它将A数的高4位屏蔽起来,使其都变成0了。 + +**总结:** + +- 仅当输入条件全部为“真”时,输出的结果为“真” +- 若输入条件有一个为“假”时,则输出结果为假 +- “与“运算符号:`*`、`^` +- 在电路中,与运算相当于开关的串联电路 + - 仅当所有开关闭合时,电路才通路。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210523155149.png) + +************* + +### 3.3“反”运算 + +又称”非“运算 + +如果一件事物的性质为A,则其经过“反”运算之后,其性质必与A相反,用表达式表示为: + +```txt +Y = !A +``` + +这实际,上也是反相器的性质。所以在电路实现上,反相器是反运算的基本元件。反运算也称为“逻辑非”或“逻辑反”。 + +```txt +当A为多位数时,如: +A = A1A2A3...An +则其“逻辑反”为: +Y= !A1!A2!A3...!An +设: A=1101 0000 +则: Y=0010 1111 +``` + +即:按位取反。 + +**总结:** + +- 当决定事件结果的条件满足时,事件不发生。 +- ”非“属于单边运算,只有一个运算对象,运算符作为一条上横线(这里我用`!`代替上横线) +- 在电路,非运算相当于短路 + - 当开关断开时灯亮,开关闭合时灯灭。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210523155150.png) + +**** + +**小结:** + +逻辑运算于数学运算的区别: + +- 算数运算是两个数之间的运算,低位运算结果将对高位运算产生影响 +- 逻辑运算是按位进行的运算,低位运算结果对于高位运算不产生影响 + +当”与“门的输入段有1位为低电平(0)时,则输出为”0“ + +当”或“门的输入端有1位为高电平(1)时,则输出为”1“ + +**** + +### 3.4布尔代数的基本运算规则 + +- 恒等式 + +```txt +A * 0 = 0 A * 1 = A A * A = A +A + 0 = A A + 1 = 1 A + A = A +A + !A = 1 A * !A = 0 !A = A +``` + +- 运算规律 + - 与普通代数一样,布尔代数也有交换律、结合律、分配律,而且它们与普通代数的规律完全相同。 + +```txt +1.交换律:A * B = B * A + A + B = B + A +2.结合律:(AB)C=A(BC)=ABC + (A+B)+C = A+(B+c) = A+B+C +3.分配律:A(B+C)=AB+AC + (A+B)(C+D) = AC+AD+BC+BD +``` + +***** + +### 3.5摩根定理 + +在电路设计时,人们手边有时没有“与”门,而只有"或"门和“非”门;或者只有“与”门和“非”门,没有“或”门。利用摩根定理,可以解决元件互换的问题。 + +二变量的摩根定理为: + +```txt +!(A+B) = !A * !B +!(A*B) = !A + !B +推广到多变量: +!(A+B+C...) = !A * !B * !C *... +!(A*B*C...) = !A + !B + !C +... +``` + +可以用表格验证此定理 + +```txt +-------------------------------------- +A B !(A+B) !A*!B !(A*B) !A+!B +-------------------------------------- +0 0 1 1 1 1 +0 1 0 0 1 1 +1 0 0 0 1 1 +1 1 0 0 0 0 +------ ------------- ------------- + ↓ ↓ ↓ +变量可能取值 相等 相等 +``` + +这个定义可以用一句话来记忆: + +头上切一刀(指的是【`!(A+B)`或`!(A*B)`】),下面变个号(指的是运算符号由`*`变`+`【或由`+`变`*`】)。 + +********** + +### 3.6真值及布尔代数式的关系 + +当人们遇到一个因果问题时,常常把各种因素全部考虑进去,然后再研究结果。真值表也就是这种方法的一种表格形式。 + +例如,考虑两个一位的二进制数A和B相加,其本位的和S及向高一位进位C的结果如何? + +全面考虑两个一位二进制数,可能出现四种情况:或A=0,B=0;或A=0,B=1;或 A=1,B=0;或A=1,B=1(一般n个因素可有2n种情况)。这实质是两个一位数(可为零,也可为1)的排列。我们可以把它们列人表内,如真值表左边部分所示。然后,对每一种情况进行分析。当A和B都为0时,S为0,进位C也为0;当A为0且B为1时,S为1,进位C为0;当A为1且B为0时,S为1,进位C为0;当A为1且B也为1时,由于S是一位数所以为0,而有进位C=1。 + +真值表 + +| A | B | S | C | +| ---- | ---- | ---- | ---- | +| 0 | 0 | 0 | 0 | +| 0 | 1 | 1 | 0 | +| 1 | 0 | 1 | 0 | +| 1 | 1 | 0 | 1 | + +逻辑问题(二进制数相加的运算由于取值只能为1,0,所以可以转化为逻辑问题)的真值表。 + +## 4. 二进制数的运算及其加法电路 + +### 4.1二进制数的相加 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210523184059.png) + +从以上例子可得出下列结论: + +(1)两个二进制数相加时,可以逐位相加。如二进制数可以写成: + +```txt +A = A3A2A1A0 +B = B3B2B1B0 +``` + +则从最右边第1位(即0权位)开始,逐位相加,其结果可以写成: + +```txt +S = S3S2S1S0 +``` + +其中各位是分别求出的: + +```txt +S0=A0+B0 → 进位C1 + +S1=A1+B1+C1 → 进位C2 + +S2=A2+B2+C2 → 进位C3 + +S3=A3+B3+C3 → 进位C4; +``` + +最后所得的和是: + +```txt +C4S3S2S1=A+B +``` + +(2)右边第1位相加的电路要求: + +- 输入量为两个,即A0及B0; + +- 输出量为两个,即S0及C1。 + +这样的一个二进制位相加的电路称为**半加器(half adder)**。 + +(3)从右边第2位开始,各位可以对应相加。各位对应相加时的电路要求: + +- 输人量为3个,即Ai,Bi,Ci + +- 输出量为两个,即Si,Ci+1。 + +其中`i=1,2,3,..,n`。这样的一个二进制位相加的电路称为**全加器(full adder)**。 + +### 4.2半加器电路 + +要求有两个输入端,用以两个代表数字(A0,B0)的电位输入;有两个输出端,用以输出总和S0及进位C1。 + +这样的电路可能出现的状态可以用图1-4中的表来表示。此表在布尔代数中称为真值表。 + +考察一下C与A0及B0之关系,即可看出这是“与”的关系,即: + +```txt +C1=A0*B0 +``` + +再看一下S0与A0及B0之关系,也可看出这是“异或”的关系,即: + +```txt +S0=A0⊕B0 +=(!A0)B0+A0(!B0) +``` + +即只有当A0及B0二者相异时,才起到或的作用;二者相同时,则其结果为0。因此,可以用“与门”及“异或门”(或称“异门”)来实现真值表的要求。图1-4就是这个真值表及半加器的电路图。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210523193347.png) + +### 4.3全加器电路 + +全加器电路的要求是:有3个输人端,以输入A赋给Ai,Bi和Ci,有两个输出端,即Si及Ci+1。 + +其真值表可以写成如图1-5所示。由此表分析可见,其总和Si可用“异或门”来实现,而其进位Ci+1则可以用3个“与门”及一个“或门”来实现,其电路图也画在图1-5中。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210523194958.png) + +这里遇到了3个输人的“异或门”的问题。如何判断多输人的“异或门”的输人与输出的关系呢? + +判断的方法是:多输入A,B,C,D,...中为“1”的输人量的个数为零及偶数时,输出为0;为奇数时,输出为1。 + +### 4.4半加器及全加器符号 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210523195315.png) + +### 4.5二进制数的加法电路 + +```txt +设: +A = 1010 =10(D) +B = 1011 =11(D) +``` + +则可安排如图1-7所示的加法电路。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210523195543.png) + +A与B相加,写成竖式算法如下: + +```txt +A: 1 0 1 0 +B: 1 0 1 1 (+ +---------------- +S: 1 0 1 0 1 +``` + +即其相加结果为S=10101。 + +从加法电路,可以看到同样的结果:S = C4 S3 S2 S1 S0 = 10101 + +### 4.6二进制数的减法运算 + +在微型计算机中,没有专用的减法器,而是将减法运算改变为加法运算。 + +其原理是:将减数B变成其补码后,再与被减数A相加,其和(如有进位的话,则舍去进位)就是两数之差。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210524175249.png) + +### 4.7可控反相器及加法/减法电路 + +利用补码可将减法变为加法来运算,因此需要有这么一个电路,它能将原码变成反码,并使其最小位加1。 + +图1-8的可控反相器就是为了使原码变为反码而设计的。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210524180036.png) + +这实际上是一个异或门(异门),两输入端的异或门的特点是:两者相同则输出为0,两者不同则输出为1。用真值表来表示这个关系,更容易看到其意义(表1-3)。 + +由此真值表可见,如将 SUB端看作控制端,则当在SUB端加上低电位时,Y端的电平就和B0端的电平相同。在SUB端加上高电平,则Y端的电平和B0端的电平相反。 + +可控相反器的真值表(表1-1) + +| SUB | B0 | Y | Y与B0的关系 | | +| ---- | ---- | ---- | ----------- | ---- | +| 0 | 0 | 0 | Y与B0相同 | 相同 | +| 0 | 1 | 1 | Y与B0相同 | 相同 | +| 1 | 0 | 1 | Y与B0相反 | 相反 | +| 1 | 1 | 0 | Y与B0相反 | 相反 | + +利用这个特点,在图1-7的4位二进制数加法电路上增加4个可控反相器并将最低位的半加器也改用全加器,就可以得到如图1-9的4位二进制数加法器/减法器电路了,因为这个电路既可以作为加法器电路(当SUB = 0),又可以作为减法器电路(当SUB=1)。 +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210524181200.png) +如果有下面两个二进制数: + +```txt +A = A3 A2 A1 A0 +B = B3 B2 B1 B0 +``` + +则可将这两个数的各位分别送入该电路的对应端,于是: + +- 当SUB=0时,电路作加法运算:A+B。 +- 当SUB=1时,电路作减法运算:A - B。 + +图1-9电路的原理如下:当SUB=0时,各位的可控反相器的输出与B的各位同相,所以图1-9和图1-7的原理完全一样,各位均按位相加。 + +```txt +结果:S = S3 S2 S1 S0 +而其和为:C4S = C4 S3 S2 S1 S0 +``` + +当SUB=1,各位的反相器的输出与B的各位反相。注意,最右边第一位(即S0位)也是用全加器,其进位输人端与SUB端相连,因此其C0 = SUB = 1。 + +所以此位的相加即为: + +```txt +A0 + (!B0) + 1 + +其他各位为: +A1 + (!B1) + C1 +A2 + (!B2) + C2 +A3 + (!B3) + C3 + +因此其总和输出S=S3 S2 S1 S0,即: +S = A + (!B) + 1 + = A3 A2 A1 A0 + (!B3) (!B2) (!B1) (!B0) + 1 + = A + B' + = A - B +``` + +当然,此时C4如不等于0,则要被舍去。 + diff --git "a/docs/60.SMC&P/03.\347\254\254\344\272\214\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\345\237\272\346\234\254\347\273\204\346\210\220\347\224\265\350\267\257.md" "b/docs/60.SMC&P/03.\347\254\254\344\272\214\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\345\237\272\346\234\254\347\273\204\346\210\220\347\224\265\350\267\257.md" new file mode 100644 index 00000000..a678ae2c --- /dev/null +++ "b/docs/60.SMC&P/03.\347\254\254\344\272\214\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\345\237\272\346\234\254\347\273\204\346\210\220\347\224\265\350\267\257.md" @@ -0,0 +1,62 @@ +--- +title: 第二章 微型计算机的基本组成电路 +date: 2021-05-16 15:30:00 +permalink: /SMC&P/note/2 +author: + name: eric + href: https://wfmiss.cn +--- + +# 第二章 微型计算机的基本组成电路 + +任何一个复杂的电路系统都可以划分为若干电路,这些电路大都由一些典型的电路组成。微型计算机就是由若干典型电路通过精心设计而组成的,各个典型电路在整体电路系统中又称为基本电路部件。 + +## 1算数逻辑单元(ALU) + +算数逻辑单元(ALU)又称:Arithmatic Logical Unit。 + +顾名思义,这个部件既能进行二进制数的四则运算,也能进行布尔代数的逻辑运算。第1章已讲过,二进制数的运算电路只能算加法。增加可控反相器后,又能进行减法,所以上章最后介绍的二进制补码加法器 / 减法器就是最简单的算术部件。 + +只要利用适当的软件配合,乘法也可以变成加法来运算,除法也可变成减法来运算。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210524191424.png) + +如果在这个基础上,增加一些门电路,也可使简单的ALU进行逻辑运算。所谓逻辑运算就是指“与”运算和“或”运算。 + +ALU的符号一般画成图2-1那样。A和B为两个二进制数,S为其运算结果,control为控制信号(见图1-9的控制线端SUB)。 + +## 2触发器(trigger) + +触发器(trigger)是计算机的记忆装置的基本单元,也可说是记忆细胞。触发器可以组成寄存器,寄存器又可以组成存储器。寄存器和存储器统称为计算机的记忆装置。 + +微型计算机所用触发器一般用晶体管元件而不用磁性元件。这是因为晶体管元件可以制成大规模的集成电路,体积可以更小些。 + +### 2.1RS触发器 + +RS触发器可以用两个与非门来组成,如图2-2所示。 + +当S=1而R=0时,Q=1(!Q=0)称为置位;当S=0而R=1时,Q=0(!Q=1)称为复位。 + +为了作图方便,以后我们就只用方块来表示,如图2-3就是RS触发器的符号。 + +S端一般称为置位端,使Q=1(!Q=0), + +R端一般称为复位端,使Q=0(!Q=1)。 + +时标RS触发器 —— 为了使触发器在整个机器中能和其他部件协调工作,RS触发器经常有外加的时标脉冲,如图2-4所示。 + +![](https://cdn.jsdelivr.net/gh/wfmiss/pictures/Principle_and_application_of_microcomputer/20210524194137.png) + +此图中的CLK即为时标脉冲。它与置位信号脉冲S同时加到一个与门的两个输入端;而与复位信号脉冲同时加到另一个与门的两个输人端。这样,无论是置位还是复位,都必须在时标脉冲端为高电位时才能进行。 + +### 2.2D触发器 + +### 2.3JK触发器 + +## 3寄存器(register) + +## 4三态输出电路 + +## 5总线结构 + +## 6存储器(memory) diff --git "a/docs/60.SMC&P/04.\347\254\254\344\270\211\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\345\237\272\346\234\254\345\267\245\344\275\234\345\216\237\347\220\206.md" "b/docs/60.SMC&P/04.\347\254\254\344\270\211\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\345\237\272\346\234\254\345\267\245\344\275\234\345\216\237\347\220\206.md" new file mode 100644 index 00000000..c2a30a79 --- /dev/null +++ "b/docs/60.SMC&P/04.\347\254\254\344\270\211\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\345\237\272\346\234\254\345\267\245\344\275\234\345\216\237\347\220\206.md" @@ -0,0 +1,8 @@ +--- +title: 第三章 微型计算机的基本工作原理 +date: 2021-05-16 15:30:00 +permalink: /SMC&P/note/3 +author: + name: eric + href: https://wfmiss.cn +--- diff --git "a/docs/60.SMC&P/05.\347\254\254\345\233\233\347\253\24016\344\275\215\345\276\256\345\244\204\347\220\206\345\231\250.md" "b/docs/60.SMC&P/05.\347\254\254\345\233\233\347\253\24016\344\275\215\345\276\256\345\244\204\347\220\206\345\231\250.md" new file mode 100644 index 00000000..97b9b357 --- /dev/null +++ "b/docs/60.SMC&P/05.\347\254\254\345\233\233\347\253\24016\344\275\215\345\276\256\345\244\204\347\220\206\345\231\250.md" @@ -0,0 +1,15 @@ +--- +title: 第四章 16位微处理器 +date: 2021-05-16 15:30:00 +permalink: /SMC&P/note/4 +author: + name: eric + href: https://wfmiss.cn +--- + +# 第四章 16位微处理器 + +## 1. 16位微处理器概述 + +## 2. 8086/8088微型处理器(CPU) + diff --git "a/docs/60.SMC&P/06.\347\254\254\344\272\224\347\253\24086\347\263\273\345\210\227\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\346\214\207\344\273\244\347\263\273\347\273\237.md" "b/docs/60.SMC&P/06.\347\254\254\344\272\224\347\253\24086\347\263\273\345\210\227\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\346\214\207\344\273\244\347\263\273\347\273\237.md" new file mode 100644 index 00000000..43fe003f --- /dev/null +++ "b/docs/60.SMC&P/06.\347\254\254\344\272\224\347\253\24086\347\263\273\345\210\227\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\346\214\207\344\273\244\347\263\273\347\273\237.md" @@ -0,0 +1,8 @@ +--- +title: 第五章 86系列微型计算机的指令系统 +date: 2021-05-16 15:30:00 +permalink: /w/note/5 +author: + name: eric + href: https://wfmiss.cn +--- diff --git "a/docs/60.SMC&P/07.\347\254\254\345\205\255\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\347\250\213\345\272\217\350\256\276\350\256\241.md" "b/docs/60.SMC&P/07.\347\254\254\345\205\255\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\347\250\213\345\272\217\350\256\276\350\256\241.md" new file mode 100644 index 00000000..1a1efc6b --- /dev/null +++ "b/docs/60.SMC&P/07.\347\254\254\345\205\255\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\347\232\204\347\250\213\345\272\217\350\256\276\350\256\241.md" @@ -0,0 +1,8 @@ +--- +title: 第六章 微型计算机的程序设计 +date: 2021-05-16 15:30:00 +permalink: /w/note/6 +author: + name: eric + href: https://wfmiss.cn +--- diff --git "a/docs/60.SMC&P/08.\347\254\254\344\270\203\347\253\240 \345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\346\261\207\347\274\226\350\257\255\350\250\200\345\217\212\346\261\207\347\274\226\347\250\213\345\272\217.md" "b/docs/60.SMC&P/08.\347\254\254\344\270\203\347\253\240 \345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\346\261\207\347\274\226\350\257\255\350\250\200\345\217\212\346\261\207\347\274\226\347\250\213\345\272\217.md" new file mode 100644 index 00000000..69b891e4 --- /dev/null +++ "b/docs/60.SMC&P/08.\347\254\254\344\270\203\347\253\240 \345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\346\261\207\347\274\226\350\257\255\350\250\200\345\217\212\346\261\207\347\274\226\347\250\213\345\272\217.md" @@ -0,0 +1,8 @@ +--- +title: 第七章 微型计算机汇编语言及汇编程序 +date: 2021-05-16 15:30:00 +permalink: /w/note/7 +author: + name: eric + href: https://wfmiss.cn +--- diff --git "a/docs/60.SMC&P/09.\347\254\254\345\205\253\347\253\240\350\276\223\345\205\245\350\276\223\345\207\272\346\216\245\345\217\243.md" "b/docs/60.SMC&P/09.\347\254\254\345\205\253\347\253\240\350\276\223\345\205\245\350\276\223\345\207\272\346\216\245\345\217\243.md" new file mode 100644 index 00000000..3bff1cbe --- /dev/null +++ "b/docs/60.SMC&P/09.\347\254\254\345\205\253\347\253\240\350\276\223\345\205\245\350\276\223\345\207\272\346\216\245\345\217\243.md" @@ -0,0 +1,9 @@ +--- +title: 第八章 输入/输出接口 +date: 2021-05-16 15:30:00 +permalink: /w/note/8 +author: + name: eric + href: https://wfmiss.cn +--- + diff --git "a/docs/60.SMC&P/10 .\347\254\254\344\271\235\347\253\240\344\270\255\346\226\255\346\216\247\345\210\266\345\231\250,\350\256\241\346\225\260\345\256\232\346\227\266\345\231\250\345\217\212DMA\346\216\247\345\210\266\345\231\250.md" "b/docs/60.SMC&P/10 .\347\254\254\344\271\235\347\253\240\344\270\255\346\226\255\346\216\247\345\210\266\345\231\250,\350\256\241\346\225\260\345\256\232\346\227\266\345\231\250\345\217\212DMA\346\216\247\345\210\266\345\231\250.md" new file mode 100644 index 00000000..11f3488e --- /dev/null +++ "b/docs/60.SMC&P/10 .\347\254\254\344\271\235\347\253\240\344\270\255\346\226\255\346\216\247\345\210\266\345\231\250,\350\256\241\346\225\260\345\256\232\346\227\266\345\231\250\345\217\212DMA\346\216\247\345\210\266\345\231\250.md" @@ -0,0 +1,9 @@ +--- +title: 第九章 中断控制器、计数/定时器及DMA控制器 +date: 2021-05-16 15:30:00 +permalink: /w/note/9 +author: + name: eric + href: https://wfmiss.cn +--- + diff --git "a/docs/60.SMC&P/11.\347\254\254\345\215\201\347\253\240AD\345\217\212DA\350\275\254\346\215\242\345\231\250.md" "b/docs/60.SMC&P/11.\347\254\254\345\215\201\347\253\240AD\345\217\212DA\350\275\254\346\215\242\345\231\250.md" new file mode 100644 index 00000000..6a0a1c32 --- /dev/null +++ "b/docs/60.SMC&P/11.\347\254\254\345\215\201\347\253\240AD\345\217\212DA\350\275\254\346\215\242\345\231\250.md" @@ -0,0 +1,10 @@ +--- +title: 第十章 A/D及D/A转换器 +date: 2021-05-16 15:30:00 +permalink: /w/note/10 +author: + name: eric + href: https://wfmiss.cn +--- + + diff --git "a/docs/60.SMC&P/12.\347\254\254\345\215\201\344\270\200\347\253\24032\344\275\215\345\276\256\345\244\204\347\220\206\345\231\250.md" "b/docs/60.SMC&P/12.\347\254\254\345\215\201\344\270\200\347\253\24032\344\275\215\345\276\256\345\244\204\347\220\206\345\231\250.md" new file mode 100644 index 00000000..8ee93928 --- /dev/null +++ "b/docs/60.SMC&P/12.\347\254\254\345\215\201\344\270\200\347\253\24032\344\275\215\345\276\256\345\244\204\347\220\206\345\231\250.md" @@ -0,0 +1,9 @@ +--- +title: 第十一章 32位微处理器 +date: 2021-05-16 15:30:00 +permalink: /w/note/11 +author: + name: eric + href: https://wfmiss.cn +--- + diff --git "a/docs/60.SMC&P/13.\347\254\254\345\215\201\344\272\214\347\253\240PC\346\200\273\347\272\277\345\217\212\346\225\264\346\234\272\347\273\223\346\236\204.md" "b/docs/60.SMC&P/13.\347\254\254\345\215\201\344\272\214\347\253\240PC\346\200\273\347\272\277\345\217\212\346\225\264\346\234\272\347\273\223\346\236\204.md" new file mode 100644 index 00000000..273df873 --- /dev/null +++ "b/docs/60.SMC&P/13.\347\254\254\345\215\201\344\272\214\347\253\240PC\346\200\273\347\272\277\345\217\212\346\225\264\346\234\272\347\273\223\346\236\204.md" @@ -0,0 +1,8 @@ +--- +title: 第十二章 PC总线及整机结构 +date: 2021-05-16 15:30:00 +permalink: /w/note/12 +author: + name: eric + href: https://wfmiss.cn +--- diff --git "a/docs/60.SMC&P/14.\347\254\254\345\215\201\344\270\211\347\253\240MCS-51\345\215\225\347\211\207\350\256\241\347\256\227\346\234\272.md" "b/docs/60.SMC&P/14.\347\254\254\345\215\201\344\270\211\347\253\240MCS-51\345\215\225\347\211\207\350\256\241\347\256\227\346\234\272.md" new file mode 100644 index 00000000..009423fd --- /dev/null +++ "b/docs/60.SMC&P/14.\347\254\254\345\215\201\344\270\211\347\253\240MCS-51\345\215\225\347\211\207\350\256\241\347\256\227\346\234\272.md" @@ -0,0 +1,8 @@ +--- +title: 第十三章 MCS-51单片计算机 +date: 2021-05-16 15:30:00 +permalink: /w/note/13 +author: + name: eric + href: https://wfmiss.cn +--- diff --git "a/docs/60.SMC&P/15.\347\254\254\345\215\201\345\233\233\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\345\234\250\350\207\252\345\212\250\346\216\247\345\210\266\347\263\273\347\273\237\344\270\255\347\232\204\345\272\224\347\224\250.md" "b/docs/60.SMC&P/15.\347\254\254\345\215\201\345\233\233\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\345\234\250\350\207\252\345\212\250\346\216\247\345\210\266\347\263\273\347\273\237\344\270\255\347\232\204\345\272\224\347\224\250.md" new file mode 100644 index 00000000..2212f8fa --- /dev/null +++ "b/docs/60.SMC&P/15.\347\254\254\345\215\201\345\233\233\347\253\240\345\276\256\345\236\213\350\256\241\347\256\227\346\234\272\345\234\250\350\207\252\345\212\250\346\216\247\345\210\266\347\263\273\347\273\237\344\270\255\347\232\204\345\272\224\347\224\250.md" @@ -0,0 +1,8 @@ +--- +title: 第十四章 微型计算机在自动控制系统中的应用 +date: 2021-05-16 15:30:00 +permalink: /w/note/14 +author: + name: eric + href: https://wfmiss.cn +--- diff --git a/docs/@pages/archivesPage.md b/docs/@pages/archivesPage.md new file mode 100644 index 00000000..c021f6b5 --- /dev/null +++ b/docs/@pages/archivesPage.md @@ -0,0 +1,6 @@ +--- +archivesPage: true +title: 归档 +permalink: /archives/ +article: false +--- \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..b7ac7939 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,35 @@ +--- +home: true +heroImage: /img/web.png +heroText: OddFar's Docs +tagline: 我们在黑暗中并肩前行,走在各自的朝圣路上! +actionText: 立刻进入 → +actionLink: /java/ +bannerBg: none +# bannerBg: auto # auto => 网格纹背景(有bodyBgImg时无背景),默认 | none => 无 | '大图地址' | background: 自定义背景样式 提示:如发现文本颜色不适应你的背景时可以到palette.styl修改$bannerTextColor变量 + + +# 文章列表显示方式: detailed 默认,显示详细版文章列表(包括作者、分类、标签、摘要、分页等)| simple => 显示简约版文章列表(仅标题和日期)| none 不显示文章列表 +postList: simple +--- + + + +::: demo [vanilla] +```html + +
+ + + +``` +::: + + diff --git a/package.json b/package.json new file mode 100644 index 00000000..1bf484e0 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "theme-vdoing-blog", + "version": "1.0.0", + "scripts": { + "dev": "vuepress dev docs", + "build": "vuepress build docs", + "deploy": "bash deploy.sh", + "editFm": "node utils/editFrontmatter.js" + }, + "license": "MIT", + "devDependencies": { + "inquirer": "^7.1.0", + "json2yaml": "^1.1.0", + "moment": "^2.25.3", + "vuepress": "^1.4.1", + "vuepress-plugin-demo-block": "^0.7.2", + "vuepress-plugin-one-click-copy": "^1.0.2", + "vuepress-plugin-thirdparty-search": "^1.0.2", + "vuepress-plugin-zooming": "^1.1.7", + "vuepress-theme-vdoing": "^1.8.3", + "yamljs": "^0.3.0" + } +} diff --git a/utils/baiduPush.js b/utils/baiduPush.js new file mode 100644 index 00000000..7ed2df01 --- /dev/null +++ b/utils/baiduPush.js @@ -0,0 +1,35 @@ +/** + * 生成百度链接推送文件 + */ +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk') +const matter = require('gray-matter'); // FrontMatter解析器 https://github.com/jonschlinkert/gray-matter +const readFileList = require('./modules/readFileList'); +const urlsRoot = path.join(__dirname, '..', 'urls.txt'); // 百度链接推送文件 +const DOMAIN = process.argv.splice(2)[0]; // 获取命令行传入的参数 + +if (!DOMAIN) { + console.log(chalk.red('请在运行此文件时指定一个你要进行百度推送的域名参数,例:node utils/baiduPush.js https://xugaoyi.com')) + return +} + +main(); + +/** + * 主体函数 + */ +function main() { + fs.writeFileSync(urlsRoot, DOMAIN) + const files = readFileList(); // 读取所有md文件数据 + + files.forEach( file => { + const { data } = matter(fs.readFileSync(file.filePath, 'utf8')); + + if (data.permalink) { + const link = `\r\n${DOMAIN}${data.permalink}`; + console.log(link) + fs.appendFileSync(urlsRoot, link); + } + }) +} diff --git a/utils/config.yml b/utils/config.yml new file mode 100644 index 00000000..e0f32932 --- /dev/null +++ b/utils/config.yml @@ -0,0 +1,16 @@ +#批量添加和修改、删除front matter配置文件 + +# 需要批量处理的路径,docs文件夹内的文件夹 (数组。映射路径:docs/arr[0]/arr[1] ... ) +path: + - docs # 第一个成员必须是docs + - 01.前端 + - 40.学习笔记 + +# 要删除的字段 (数组) +delete: + - test + # - tags + + # 要添加、修改front matter的数据 (front matter中没有的数据则添加,已有的数据则覆盖) +data: + # test: 阮一峰 \ No newline at end of file diff --git a/utils/editFrontmatter.js b/utils/editFrontmatter.js new file mode 100644 index 00000000..8c223f4e --- /dev/null +++ b/utils/editFrontmatter.js @@ -0,0 +1,92 @@ +/** + * 批量添加和修改front matter ,需要配置 ./config.yml 文件。 + */ +const fs = require('fs'); // 文件模块 +const path = require('path'); // 路径模块 +const matter = require('gray-matter'); // front matter解析器 https://github.com/jonschlinkert/gray-matter +const jsonToYaml = require('json2yaml') +const yamlToJs = require('yamljs') +const inquirer = require('inquirer') // 命令行操作 +const chalk = require('chalk') // 命令行打印美化 +const readFileList = require('./modules/readFileList'); +const { type, repairDate} = require('./modules/fn'); +const log = console.log + +const configPath = path.join(__dirname, 'config.yml') // 配置文件的路径 + +main(); + +/** + * 主体函数 + */ +async function main() { + + const promptList = [{ + type: "confirm", + message: chalk.yellow('批量操作frontmatter有修改数据的风险,确定要继续吗?'), + name: "edit", + }]; + let edit = true; + + await inquirer.prompt(promptList).then(answers => { + edit = answers.edit + }) + + if(!edit) { // 退出操作 + return + } + + const config = yamlToJs.load(configPath) // 解析配置文件的数据转为js对象 + + if (type(config.path) !== 'array') { + log(chalk.red('路径配置有误,path字段应该是一个数组')) + return + } + + if (config.path[0] !== 'docs') { + log(chalk.red("路径配置有误,path数组的第一个成员必须是'docs'")) + return + } + + const filePath = path.join(__dirname, '..', ...config.path); // 要批量修改的文件路径 + const files = readFileList(filePath); // 读取所有md文件数据 + + files.forEach(file => { + let dataStr = fs.readFileSync(file.filePath, 'utf8');// 读取每个md文件的内容 + const fileMatterObj = matter(dataStr) // 解析md文件的front Matter。 fileMatterObj => {content:'剔除frontmatter后的文件内容字符串', data:{}, ...} + let matterData = fileMatterObj.data; // 得到md文件的front Matter + + let mark = false + // 删除操作 + if (config.delete) { + if( type(config.delete) !== 'array' ) { + log(chalk.yellow('未能完成删除操作,delete字段的值应该是一个数组!')) + } else { + config.delete.forEach(item => { + if (matterData[item]) { + delete matterData[item] + mark = true + } + }) + + } + } + + // 添加、修改操作 + if (type(config.data) === 'object') { + Object.assign(matterData, config.data) // 将配置数据合并到front Matter对象 + mark = true + } + + // 有操作时才继续 + if (mark) { + if(matterData.date && type(matterData.date) === 'date') { + matterData.date = repairDate(matterData.date) // 修复时间格式 + } + const newData = jsonToYaml.stringify(matterData).replace(/\n\s{2}/g,"\n").replace(/"/g,"") + '---\r\n' + fileMatterObj.content; + fs.writeFileSync(file.filePath, newData); // 写入 + log(chalk.green(`update frontmatter:${file.filePath} `)) + } + + }) +} diff --git a/utils/modules/fn.js b/utils/modules/fn.js new file mode 100644 index 00000000..48cbbd17 --- /dev/null +++ b/utils/modules/fn.js @@ -0,0 +1,21 @@ +// 类型判断 +exports.type = function (o){ + var s = Object.prototype.toString.call(o) + return s.match(/\[object (.*?)\]/)[1].toLowerCase() +} + + // 修复date时区格式的问题 + exports.repairDate = function (date) { + date = new Date(date); + return `${date.getUTCFullYear()}-${zero(date.getUTCMonth()+1)}-${zero(date.getUTCDate())} ${zero(date.getUTCHours())}:${zero(date.getUTCMinutes())}:${zero(date.getUTCSeconds())}`; +} + +// 日期的格式 +exports.dateFormat = function (date) { + return `${date.getFullYear()}-${zero(date.getMonth()+1)}-${zero(date.getDate())} ${zero(date.getHours())}:${zero(date.getMinutes())}:${zero(date.getSeconds())}` +} + +// 小于10补0 +function zero(d){ + return d.toString().padStart(2,'0') +} \ No newline at end of file diff --git a/utils/modules/readFileList.js b/utils/modules/readFileList.js new file mode 100644 index 00000000..8eb97c62 --- /dev/null +++ b/utils/modules/readFileList.js @@ -0,0 +1,43 @@ +/** + * 读取所有md文件数据 + */ +const fs = require('fs'); // 文件模块 +const path = require('path'); // 路径模块 +const docsRoot = path.join(__dirname, '..', '..', 'docs'); // docs文件路径 + +function readFileList(dir = docsRoot, filesList = []) { + const files = fs.readdirSync(dir); + files.forEach( (item, index) => { + let filePath = path.join(dir, item); + const stat = fs.statSync(filePath); + if (stat.isDirectory() && item !== '.vuepress') { + readFileList(path.join(dir, item), filesList); //递归读取文件 + } else { + if(path.basename(dir) !== 'docs'){ // 过滤docs目录级下的文件 + + const fileNameArr = path.basename(filePath).split('.') + let name = null, type = null; + if (fileNameArr.length === 2) { // 没有序号的文件 + name = fileNameArr[0] + type = fileNameArr[1] + } else if (fileNameArr.length === 3) { // 有序号的文件 + name = fileNameArr[1] + type = fileNameArr[2] + } else { // 超过两个‘.’的 + log(chalk.yellow(`warning: 该文件 "${filePath}" 没有按照约定命名,将忽略生成相应数据。`)) + return + } + if(type === 'md'){ // 过滤非md文件 + filesList.push({ + name, + filePath + }); + } + + } + } + }); + return filesList; +} + +module.exports = readFileList; \ No newline at end of file