├── .nojekyll ├── LICENSE ├── README.md ├── _coverpage.md ├── _sidebar.md ├── drawio └── 实战问题 │ ├── 布隆过滤器初级讲解.drawio │ └── 领取礼品结构以及分布式锁.drawio ├── index.html ├── java基础 ├── Java反射使用说透彻一点.md ├── Lambda │ └── Lambda在List中的简单用法.md ├── instanceof用法详解.md ├── isAssignableFrom的用法详解.md ├── java native关键字.md ├── java接口与抽象类异同分析.md ├── java集合基础 │ ├── 1.俯瞰java集合分类.md │ ├── 10.LinkedList源码解析.md │ ├── 11.ArrayList,Vector,LinkedList的区别是什么?.md │ ├── 2.iterator接口.md │ ├── 3.iterable接口.md │ ├── 4.iterator和iterable异同详解.md │ ├── 5.Collections源码解析.md │ ├── 6.1Collections和Collection的区别.md │ ├── 6.Collection接口详解.md │ ├── 7.List接口详解.md │ ├── 8.ArrayList源码分析.md │ └── 9.Vector源码解析.md ├── serialVersionUID.md ├── transient关键字作用.md └── 函数内交换数值会不会变?.md ├── 分布式 ├── 分布式系统唯一id怎么生成.md ├── 雪花算法对System.currentTimeMillis()的优化.md └── 雪花算法的细枝末节讲解.md ├── 工具 ├── 使用PicGo存储markdown图片(阿里云或者github).md ├── 如何使用docsify搭建github文档.md └── 平时画图的软件.md ├── 并发与多线程 └── 线程池 │ ├── 如何设计一个线程池.md │ ├── 线程池的那些事1.md │ └── 线程池的那些事2.md ├── 数据库 └── JDBC │ ├── 1.jdbc教程入门之增删改查.md │ ├── 2.JDBC工作原理以及简单封装.md │ ├── 3.SPI技术以及数据库连接中的使用.md │ ├── 4.SPI底层原理解析.md │ └── 5.jdbc预编译与拼接sql对比.md ├── 架构设计 ├── 布隆过滤器的三种实践.md ├── 布隆过滤器详解.md ├── 并发中分布式锁setnx解析.md ├── 海量ip中查找出现次数最多的一个.md ├── 设计一个短链接生成系统.md └── 设计领取礼品的架构以及并发问题解决.md ├── 缓存 └── 缓存穿透,缓存击穿,缓存雪崩的区别以及解决方案.md └── 设计模式 ├── 设计模式【1.1】-- 饿汉式单例为什么是线程安全的.md ├── 设计模式【1.2】--如何破坏单例模式.md ├── 设计模式【1.3】--枚举的单例模式真的是安全的么?.md ├── 设计模式【1】--设计模式之单例.md ├── 设计模式【2.1】--工厂方法模式.md ├── 设计模式【2.2】--抽象工厂模式.md ├── 设计模式【2】--简单工厂模式.md ├── 设计模式【3.2】--JDK动态代理到底有多香之源码分析.md ├── 设计模式【3.3】--CGLib源码分析.md ├── 设计模式【3】--代理模式.md ├── 设计模式【4】--建造者模式.md ├── 设计模式【5】--原型模式.md ├── 设计模式【6.1】--初探适配器模式.md ├── 设计模式【6.2】--再谈适配器模式.md ├── 设计模式【7】--桥接模式.md ├── 设计模式【8】--装饰器模式.md └── 设计模式【9】--外观模式.md /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damaer/Coding/bc3b8d36b248c99b34de974025f122a51083596f/.nojekyll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 wenhaoxu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coding 2 | 3 | 目前持续更新中... 4 | 5 | ### 在线阅读地址 6 | 7 | GitHub Pages 完整刷题笔记阅读:[进入](https://damaer.github.io/Coding/#/) 8 | 9 | * Java基础 10 | * 基础语法 11 | * [函数内交换数值会不会变?](/java基础/函数内交换数值会不会变?.md) 12 | * [java接口与抽象类异同分析](/java基础/java接口与抽象类异同分析.md) 13 | * [serialVersionUID的用法详解](/java基础/serialVersionUID.md) 14 | * 关键字 15 | * [transient 关键字作用](/java基础/transient关键字作用.md) 16 | * [instanceof 用法详解](/java基础/instanceof用法详解.md) 17 | * [java native关键字干什么用的?](/java基础/native关键字.md) 18 | * 特殊方法源码解读 19 | * [isAssignableFrom()的用法详解](/java基础/isAssignableFrom的用法详解.md) 20 | * Lambda表达式 21 | * [Lambda表达式简单用法](/java基础/Lambda/Lambda在List中的简单用法.md) 22 | * 反射 23 | * [Java反射使用说透彻一点](/java基础/Java反射使用说透彻一点.md) 24 | * 集合 25 | * [1.俯瞰java集合分类](/java基础/java集合基础/1.俯瞰java集合分类.md) 26 | * [2.iterator接口](/java基础/java集合基础/2.iterator接口.md) 27 | * [3.iterable接口](/java基础/java集合基础/3.iterable接口.md) 28 | * [4.iterator和iterable异同详解](/java基础/java集合基础/4.iterator和iterable异同详解.md) 29 | * [5.Collections源码解析](/java基础/java集合基础/5.Collections源码解析.md) 30 | * [6.Collection接口详解](/java基础/java集合基础/6.Collection接口详解.md) 31 | * [6.1Collections和Collection的区别](/java基础/java集合基础/6.1Collections和Collection的区别.md) 32 | * [7.List接口详解](/java基础/java集合基础/7.List接口详解.md) 33 | * [8.ArrayList源码分析](/java基础/java集合基础/8.ArrayList源码分析.md) 34 | * [9.Vector源码解析](/java基础/java集合基础/9.Vector源码解析.md) 35 | * [10.LinkedList源码解析](/java基础/java集合基础/10.LinkedList源码解析.md) 36 | * [11.ArrayList,Vector,LinkedList的区别是什么?](/java基础/java集合基础/11.ArrayList,Vector,LinkedList的区别是什么?.md) 37 | 38 | 39 | * 设计模式 40 | * 单例模式 41 | * [单例介绍](/设计模式/设计模式【1】--设计模式之单例.md) 42 | * [饿汉式单例为什么是线程安全的](/设计模式/设计模式【1.1】--饿汉式单例为什么是线程安全的.md) 43 | * [如何破坏单例模式](/设计模式/设计模式【1.2】--如何破坏单例模式.md) 44 | * [推荐枚举单例的理由](/设计模式/设计模式【1.3】--枚举的单例模式真的是安全的么?.md) 45 | * 工厂模式 46 | * [简单工厂模式](/设计模式/设计模式【2】--简单工厂模式.md) 47 | * [工厂方法模式](/设计模式/设计模式【2.1】--工厂方法模.md) 48 | * [抽象工厂模式](/设计模式/设计模式【2.2】--抽象工厂模式.md) 49 | * 代理模式 50 | * [代理模式](/设计模式/设计模式【3】--代理模式.md) 51 | * [JDK代理源码分析](/设计模式/设计模式【3.2】--JDK动态代理到底有多香之源码分析.md) 52 | * [CGLib代理源码分析](/设计模式/设计模式【3.3】--CGLib源码分析.md) 53 | * 建造者模式 54 | * [建造者模式](/设计模式/建造者模式.md) 55 | * 原型模式 56 | * [原型模式](/设计模式/原型模式.md) 57 | * 适配器模式 58 | * [适配器模式](/设计模式/设计模式【6.1】--初探适配器模式.md) 59 | * [适配器模式](/设计模式/设计模式【6.2】--再谈适配器模式.md) 60 | * 桥接模式 61 | * [桥接模式](/设计模式/设计模式【7】--桥接模式.md) 62 | * 装饰器模式 63 | * [装饰器模式](/设计模式/设计模式【8】--装饰器模式.md) 64 | * 外观模式 65 | * [外观模式](/设计模式/设计模式【9】--外观模式.md) 66 | 67 | * 并发编程 68 | * 线程池 69 | * [线程池详解](/并发与多线程/线程池.md) 70 | 71 | * 数据库 72 | * JDBC 73 | * [1.jdbc教程入门之增删改查](/数据库/JDBC/1.jdbc教程入门之增删改查.md) 74 | * [2.JDBC工作原理以及简单封装](/数据库/JDBC/2.JDBC工作原理以及简单封装.md) 75 | * [3.SPI技术以及数据库连接中的使用](/数据库/JDBC/3.SPI技术以及数据库连接中的使用.md) 76 | * [4.SPI底层原理解析](/数据库/JDBC/4.SPI底层原理解析.md) 77 | * [5.jdbc预编译与拼接sql对比](/数据库/JDBC/5.jdbc预编译与拼接sql对比.md) 78 | 79 | * 缓存 80 | * [缓存穿透,缓存击穿,缓存雪崩的区别以及解决方案](/缓存/缓存穿透,缓存击穿,缓存雪崩的区别以及解决方案.md) 81 | 82 | * 系统设计 83 | * 布隆过滤器 84 | * [布隆过滤器详解](/架构设计/布隆过滤器详解.md) 85 | * [布隆过滤器的三种实践](/架构设计/布隆过滤器的三种实践.md) 86 | * 分布式锁 87 | * [设计领取礼品的架构以及并发问题解决](/架构设计/设计领取礼品的架构以及并发问题解决.md) 88 | * [并发中分布式锁setnx解析](/架构设计/并发中分布式锁setnx解析.md) 89 | * [海量ip中查找出现次数最多的一个](/架构设计/海量ip中查找出现次数最多的一个.md) 90 | * [设计一个短链接生成系统](/架构设计/设计一个短链接生成系统.md) 91 | * 分布式 92 | * 分布式唯一ID 93 | * [分布式系统唯一id怎么生成](/架构设计/分布式系统唯一id怎么生成.md) 94 | * [雪花算法的细枝末节讲解](/架构设计/雪花算法的细枝末节讲解.md) 95 | * [雪花算法对System.currentTimeMillis()的优化](/架构设计/雪花算法对System.currentTimeMillis()的优化.md) 96 | 97 | * 工具(工欲善其事必先利其器) 98 | * 学习写作工具 99 | * [平时画图的软件](/工具/平时画图的软件.md) 100 | * [如何使用docsify搭建github文档](/工具/如何使用docsify搭建github文档.md) 101 | * [使用PicGo存储markdown图片(阿里云或者github)](/工具/使用PicGo存储markdown图片(阿里云或者github).md) 102 | 103 | ### 为什么要做这个知识仓库? 104 | 105 | 技术,是技术人生活中重要的一部分。因为喜欢那种踏实的感觉,喜欢一步一个脚印,知道了更多东西的感觉,所以把学到东西当成了一种乐趣。 106 | 107 | 而这个仓库,是本人平日学习,积累,或者疑惑之后突然找到答案,或者总结对比,积攒的知识点,只要还在技术行业,就会一直更新。说不上是面试知识总结,更说不上是教别人什么,最重要的是知我所知,知我所不知。 108 | 109 | 种一棵树,最好的时间是十年前,其次,是现在。 110 | 111 | 道之所在,虽千万人吾往矣。纵使缓慢,驰而不息。 112 | 113 | ### 项目维护的大致方向 114 | 115 | - 基础知识 116 | - 设计模式 117 | - redis 118 | - jdk源码 119 | - spring 120 | - mybatis 121 | - springmvc 122 | - 分布式 123 | - mq 124 | - 微服务 125 | 126 | 仓库Github的访问比较慢,电信如果屏蔽,自行把DNS服务器指定到 **114.114.114.114**。后期可能同步到Gitee上,解决国内访问慢的问题。 127 | 128 | ### 开源协议 129 | 130 | 本着互联网的开放精神,本项目采用开放的[MIT]协议进行许可。 131 | 132 | ### 关于作者 133 | 秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。 134 | 135 | [2020年我写了什么?](http://aphysia.cn/archives/2020) 136 | 137 | [开源编程笔记](https://damaer.github.io/Coding/#/) 138 | 139 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210224232519.png) 140 | 平日上班工作,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~ 141 | 142 | ## 个人微信 143 | 如果有什么问题可以及时联系我,纠正~ 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /_coverpage.md: -------------------------------------------------------------------------------- 1 | 2 | # Coding 0.0.1 3 | > 编程知识积累 4 | 5 | - `思考 · 实践 · 对比 · 总结` 6 | 7 | ![](https://img.shields.io/badge/version-v0.0.1-green.svg) ![](https://img.shields.io/badge/author-Aphysia-yellow.svg) ![](https://img.shields.io/badge/license-MIT-blue.svg) 8 | 9 | 10 | 11 | 👁️本页访问次数: 12 | 13 | 14 | | 🧑总访客数: 15 | 16 | 17 | [GitHub](https://github.com/Damaer/Coding) 18 | [Get Started](#Coding) -------------------------------------------------------------------------------- /_sidebar.md: -------------------------------------------------------------------------------- 1 | * Java基础 2 | * 基础语法 3 | * [函数内交换数值会不会变?](/java基础/函数内交换数值会不会变?.md) 4 | * [java接口与抽象类异同分析](/java基础/java接口与抽象类异同分析.md) 5 | * [serialVersionUID的用法详解](/java基础/serialVersionUID.md) 6 | * 关键字 7 | * [transient 关键字作用](/java基础/transient关键字作用.md) 8 | * [instanceof 用法详解](/java基础/instanceof用法详解.md) 9 | * [java native关键字干什么用的?](/java基础/native关键字.md) 10 | * 特殊方法源码解读 11 | * [isAssignableFrom()的用法详解](/java基础/isAssignableFrom的用法详解.md) 12 | * Lambda表达式 13 | * [Lambda表达式简单用法](/java基础/Lambda/Lambda在List中的简单用法.md) 14 | * 反射 15 | * [Java反射使用说透彻一点](/java基础/Java反射使用说透彻一点.md) 16 | * 集合 17 | * [1.俯瞰java集合分类](/java基础/java集合基础/1.俯瞰java集合分类.md) 18 | * [2.iterator接口](/java基础/java集合基础/2.iterator接口.md) 19 | * [3.iterable接口](/java基础/java集合基础/3.iterable接口.md) 20 | * [4.iterator和iterable异同详解](/java基础/java集合基础/4.iterator和iterable异同详解.md) 21 | * [5.Collections源码解析](/java基础/java集合基础/5.Collections源码解析.md) 22 | * [6.Collection接口详解](/java基础/java集合基础/6.Collection接口详解.md) 23 | * [6.1Collections和Collection的区别](/java基础/java集合基础/6.1Collections和Collection的区别.md) 24 | * [7.List接口详解](/java基础/java集合基础/7.List接口详解.md) 25 | * [8.ArrayList源码分析](/java基础/java集合基础/8.ArrayList源码分析.md) 26 | * [9.Vector源码解析](/java基础/java集合基础/9.Vector源码解析.md) 27 | * [10.LinkedList源码解析](/java基础/java集合基础/10.LinkedList源码解析.md) 28 | * [11.ArrayList,Vector,LinkedList的区别是什么?](/java基础/java集合基础/11.ArrayList,Vector,LinkedList的区别是什么?.md) 29 | 30 | 31 | * 设计模式 32 | * 单例模式 33 | * [单例介绍](/设计模式/设计模式【1】--设计模式之单例.md) 34 | * [饿汉式单例为什么是线程安全的](/设计模式/设计模式【1.1】--饿汉式单例为什么是线程安全的.md) 35 | * [如何破坏单例模式](/设计模式/设计模式【1.2】--如何破坏单例模式.md) 36 | * [推荐枚举单例的理由](/设计模式/设计模式【1.3】--枚举的单例模式真的是安全的么?.md) 37 | * 工厂模式 38 | * [简单工厂模式](/设计模式/设计模式【2】--简单工厂模式.md) 39 | * [工厂方法模式](/设计模式/设计模式【2.1】--工厂方法模.md) 40 | * [抽象工厂模式](/设计模式/设计模式【2.2】--抽象工厂模式.md) 41 | * 代理模式 42 | * [代理模式](/设计模式/设计模式【3】--代理模式.md) 43 | * [JDK代理源码分析](/设计模式/设计模式【3.2】--JDK动态代理到底有多香之源码分析.md) 44 | * [CGLib代理源码分析](/设计模式/设计模式【3.3】--CGLib源码分析.md) 45 | * 建造者模式 46 | * [建造者模式](/设计模式/建造者模式.md) 47 | * 原型模式 48 | * [原型模式](/设计模式/原型模式.md) 49 | * 适配器模式 50 | * [适配器模式](/设计模式/设计模式【6.1】--初探适配器模式.md) 51 | * [适配器模式](/设计模式/设计模式【6.2】--再谈适配器模式.md) 52 | * 桥接模式 53 | * [桥接模式](/设计模式/设计模式【7】--桥接模式.md) 54 | * 装饰器模式 55 | * [装饰器模式](/设计模式/设计模式【8】--装饰器模式.md) 56 | * 外观模式 57 | * [外观模式](/设计模式/设计模式【9】--外观模式.md) 58 | 59 | * 并发编程 60 | * 线程池 61 | * [线程池详解](/并发与多线程/线程池.md) 62 | 63 | * 数据库 64 | * JDBC 65 | * [1.jdbc教程入门之增删改查](/数据库/JDBC/1.jdbc教程入门之增删改查.md) 66 | * [2.JDBC工作原理以及简单封装](/数据库/JDBC/2.JDBC工作原理以及简单封装.md) 67 | * [3.SPI技术以及数据库连接中的使用](/数据库/JDBC/3.SPI技术以及数据库连接中的使用.md) 68 | * [4.SPI底层原理解析](/数据库/JDBC/4.SPI底层原理解析.md) 69 | * [5.jdbc预编译与拼接sql对比](/数据库/JDBC/5.jdbc预编译与拼接sql对比.md) 70 | 71 | * 缓存 72 | * [缓存穿透,缓存击穿,缓存雪崩的区别以及解决方案](/缓存/缓存穿透,缓存击穿,缓存雪崩的区别以及解决方案.md) 73 | 74 | * 系统设计 75 | * 布隆过滤器 76 | * [布隆过滤器详解](/架构设计/布隆过滤器详解.md) 77 | * [布隆过滤器的三种实践](/架构设计/布隆过滤器的三种实践.md) 78 | * 分布式锁 79 | * [设计领取礼品的架构以及并发问题解决](/架构设计/设计领取礼品的架构以及并发问题解决.md) 80 | * [并发中分布式锁setnx解析](/架构设计/并发中分布式锁setnx解析.md) 81 | * [海量ip中查找出现次数最多的一个](/架构设计/海量ip中查找出现次数最多的一个.md) 82 | * [设计一个短链接生成系统](/架构设计/设计一个短链接生成系统.md) 83 | * 分布式 84 | * 分布式唯一ID 85 | * [分布式系统唯一id怎么生成](/架构设计/分布式系统唯一id怎么生成.md) 86 | * [雪花算法的细枝末节讲解](/架构设计/雪花算法的细枝末节讲解.md) 87 | * [雪花算法对System.currentTimeMillis()的优化](/架构设计/雪花算法对System.currentTimeMillis()的优化.md) 88 | 89 | * 工具(工欲善其事必先利其器) 90 | * 学习写作工具 91 | * [平时画图的软件](/工具/平时画图的软件.md) 92 | * [如何使用docsify搭建github文档](/工具/如何使用docsify搭建github文档.md) 93 | * [使用PicGo存储markdown图片(阿里云或者github)](/工具/使用PicGo存储markdown图片(阿里云或者github).md) 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /drawio/实战问题/布隆过滤器初级讲解.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /drawio/实战问题/领取礼品结构以及分布式锁.drawio: -------------------------------------------------------------------------------- 1 | 7V1bb+O4Ff41epyBSIoU+WgnTgt0F7voAO3sviwcW0ncdaLUVnaS/vqSkihbJCVTlkTasYHBwKIlWuE5PJfvXBigm+f3v23mr08/p8tkHcBw+R6g2wBCEEEYiH/h8qMYoXLgcbNaFkPhbuDb6n9J+aQcfVstk205VgxlabrOVq/1wUX68pIsstrYfLNJf9Rve0jXy9rA6/wxqb2GGPi2mK8T7bZ/r5bZUzkKw73b/56sHp/kTxP5zfNc3l0ObJ/my/TH3hCaBehmk6ZZ8en5/SZZi9WrL8xdw7fVm22Sl8zmgX/N/qC/z37+5R//XIHt5JfvL99//+ULi4tp/pqv38o/uXzb7EOuwSZ9e1kmYpYwQNMfT6ss+fY6X4hvf3Cq87Gn7HnNrwD/uJxvn/J7xUU5d7LJkvfGtwbVWnAuStLnJNt88FvKBxjExSMlAwFEylf+saMHjMs1ftonBS0H5yUPPFaT71aJfygXqsOiAWZYNLLmvzu95x8es/xPLwYeUv537i8n+e9bKr/4ss0ZfsJvANHr++5LdZb7jTrCX7yYWhvee4VGQoLDhHxYrdc36Trd5M+iJU7oMuLj22yT/pnsfUPhPSJkGGqjKP5ap3eEQ43cETWQO4KjkRuf+B7Bkv3lHjGsmest4mqHTIIZDvj/LApmsfh/Qj/3FgGoTuxKVf6o6Sed2GQsWkNXtJ4GMxKwm4DeCqLTSTABBgE5uwum/B4azGgwDQOKKsZYGJ4XN90FbCo+8Et6I76a4IAR8YGhgIL8Zn4PyB+PgulM/ga7OPlMFUlDDcyHTJJmNOmMDgtnbnm9io8P6+R9IoxCvhjJy7L8eLtYz7fb1aK+vHxBNh/fhTznCqm8/K0U7/nF7Xvt6qO8Kn48WUqzsmmJuR073zwm2SEZqpNib6mxYaXl2CZZz7PVX/XXMK1++Qu/pqt8b0rWoOyrankpJNymb5tFUj63b3genKoyn+VUxVpoU+UMUf3px/NINDaP1DhkxzDj8wj0ySPVPJKs0ZEcok6EgFv+IK402E1HDRaLm9mdeGpyG0yQSRXp6oo/NQloJO6hM/Ft8ZR4/EL1Fjk5o8nC7z5FkeRL1GAwkKjROAFVysmRsKH2lH97Xk8WmdgQU8H+q8V8/dP8Pln/mm5X2Sp94bfcp1mWPu/dMFmvHsUXWapsvPQtW69e+A6TSFk4zN4Cik1oMAlNO0sl4HCup0GaCwHI3UQuJLnMnEppOQ0m/CsWMFrKWFUyK2Tha5TV17QutF7Sl0SRcOXQvCTKgq9psjFQ63m1XIqfMUrPOrgwNHpAiGLTxzoBmYGA41n0obbyQjB9Ky/TTfaUPqYv8/VsN6qs0e6en1KxD/K1+k+SZR8lxDx/y1JFcr6vsu97nwu5GePycic3xYUUm+NbgIVca2N3amkqogZb0Voy9yJqhH0StaSIJGsrTQenTY8lb3CoiIqSYubWYAYmJXbF9wZRp6FKXGIQyI4Rvg4O9HnYLFqcAUeezRZo8giULWUCQEuX7kYimVFp2rBZfjMNJnf5SBhMcqtnGgdTKPYSn407hdz84UYQJdKzJOU8NLbaT5/TJIpiqIahDHsQmoDO0awiaOE3dAA1UQTrJkuI0AGjJb/6Ndms+B8kKDY43tmgLR0BnrpIUJWqrY+JoTpVrGKnI+tnaBOh3oU+cu9I4ERMSJRp/oHdCsFwuTJAs7GICTMKDQyp0no4x8gmqiqE/l1OzFz0Cw/3KDVwuZSHsbZ9TUkITl1ipMt6h96Tte9UKZhK2xTKBTp0iBHsqW/cOMQo9kFSH+uMiM91xue2dfZ3y2/7W+mEtk40CLDBbdX5x94Nr8Ic2jabaJFmohEGFQYp5hzUmJKr4iDdT2hfrqrz0B3LY378wzTHrEuHj+ZaHOYOXyzunN7sRf5Mmp5PwoqnbnqCJ3ytVq/bJqXcDJw80EWyWJiAk3uKRYLfME4b0LjDlJ1icupHS4NDJuDEYLGpJCaCDQpfnL8bN84NtOaXtEQCGkl8Nd2q6MahDKXxbDd27oq+r/5ucLjZsV5yV71BkPpLlCm58gffTXliHE0TRV5tlVqQRH5z0FrxZqwQV3b+cUynAQgsOsB0h54Yh+mwF/nUy0COO/Hc4m3zl6Jk3DKgH2uZQLMmbEYtw373h+33Ryxqu38k2705tW+gypxuwFvHaEuDrT6QXb6cJ/TBaJeTBU3uHwYCUvWApn+73CZ+3cn1MmRxMlF7ML0a6LVSJY0ZKqPPm4EukVybYMl+dLVLLHUXpdXvIWKcsjzoEgWTWT7hbfmBs9Y0vGCG0SsCWeidYUw1JybpAfMAfRGyZ3bsUWRqG3kpFlJoWjBMuCdzrlwhuMK7nx/ZVBEYxEguPbgSEXwyE6xyuWTVY/D+g67RucM30XkkiGKDFrbaLQbi5Aln9QW1Z/tNwm3/+X0+n6Be6WfxyfE0wLdtnF52oSgfDvaaSuzI1sJkjfviS/gV1rZF+XvHInDylvThYZuMkoCCoY9dcwLp2Na7DTeIQje7jejVDYfpUwNSRkm4bwcdvQjFQcDvrtiNhq2gdmyF4Nb7R0KrLZzoTbJcbUfPzH54SIg5wLiM2X04WoCxIlPNOqlyR53kZmPiRdKa4FvrahklOyh2KJ+xbTZqb9C2H1WPT/XDAjAQPmIBIRTVE2j4PVgz/r1sSKJFe40b0mmpBDZE/DU6lVLxzCzXA9JRC5mSNp/MrMcqaSa9+voEI9qreo+ngkgGGKfCajptv09FbKwRG+POxK6Hg764o7VFpc1gQM1501mP3eC2mMxp0Zm04voWgT9kztFrkOZ6AEgP/7VCwVxcCNGBBfjPxcjn5y6sVJ6TNq3coDCAwl3EGXfBRu7avs5fejDYcLKoGive6FMykdrQ4xirwx8T2USSrp1Ej4kQyRC/RExDUiqyWjlhuf61ekKJawwPunlN7zu3xhbENr0qxmZOcISkXmJjCzJSSjBWNi2ShW+uGsE56wR3cX0tmELaCCBNHruFakiHHmzn0dVC1XmQ6mvstKkFsQgRHGxqUevha8yg0logXm4KRcTq5jAIqaFfvtPEGGJqRqCqzLPuXNGkDh11rlC3vaY1rRtXAFVIx271b9wK2Vz7VtiAKiozYIOedZpBFVvFslRBb1QLerP2a9+KveJHoOxe6XDud3KEBsqjsShvEPQnXVrUtW1FwTbyZCCk29bWpc+DuGUx7qmw3LjMsZeWDD7WuQiW+FpnIJNnfWETBMQ10CmEB1AnfqGaaU1IlMukEXt6N+Rx9rTwIunH7ZxnpWd1g4XXucMGhYr1UD9UbJxEslgPvl8bbJxgg41Q9Q6gJYQzWh1fbFVzMXR/jbZiLpzPU5iqkfBNLtgeJSq/hAZ+cQpFxF6abp2YldjbKGmAIeiR4EH3wnblh0jpwDQfQNT+wDharXKyfebY+w/jSdl/0HZiXhNuQagHZwdc/m5dUg6zSXcBogshH0QeKXAYA8VuJZYN8TvLHhVeiyFslz0HHhhJ9oBj6nsutOuhNfP29u56evNeO1mCY2gadzo7sS6ioC891Lcw7yjBEssoWWU9IdwqWLQHQNkeujG4E5HWB8aSRCfU0+w4FLgbCOyLbYeBnjqzLVDZFrbb4toDqDS8RuZCL46fSR9a99WjIe1kM5pi3KfBmsMYfZ1NNaDIOxy3S1TAcOsD47AmbQ55XzuUuelQhrXcNO/AJrWs/7w2KBs8f0qteOKWGNG5wSlsKaMy7dxwXD8y06lR4uZQ8tDkincfWXJAIt94N7XqP37tSdUlqdZ/QhX1UnIwpOXILC3H3qUdx0XBlW4qbnwUajq3+FIaYNEGQp9tAywgg+W+cA94BPARfo16u5w+xERvyK4nMAv9kvqotv2IxHWYKzyYUXUy9PabVidf81L6XmFGvsaKTwRjkxFk7nwFx9p3oZ+TCxqCuoeiGwNuFGZbmOI3YsUsQYyLbmIVYTUsI3t/eSuMlDUCF9fDCuvpr51TEby1sGJ6L4lrC6sWWit9iQCUPWtPv7MRcwnrnDeZCVZS5yFoqXY6MTK35sVfG1j5Z65QBYrUlNdTbj7EmpPnrx2snIooLU8+Ph8uAuH5NCAOBnTBBjnTzZAeo1glUax0oxq5Ap9ZdOwYJj/hxlDgbdBBhalK87rwG6G4ZnnZ1gRJ1VPYtVEwnRkagYinJgHN673oLFd6xQk7SM7MLq0zGpPu3S6+7bsTDzMgakVzB3HcUZ7iUBJ1mvsoTNglBSuoDKSQ45PGJEOg4HHI0OWFuYw081eyEBxal4eG89DqO/V8gs8D0DamegKK7M+0T1xmwlpHJK/VmWeGbk1NTTz2bcFrWklbWkmksENsYAe3aSUgdNlC+zTIOsTWZmoZv6E/m+uNDUYDGUSJdVjue6Gs9844FFLCmGvaxCD7FiEWj3CTQbZ/aoIvPj/+EKNzxh8AaK3eH4P1WjCGXr9ogYZpbGsR57osZtYzbyE5K3a2PFRaZc5rC7OWc10BjnTPxrG1I2c+/fyeUYu8ARinu5F6DITej3ZkzAtAr7l69cbsxx2h1r1R3fCMYZGPAmDck4V6GrsWqVvDoJtcyby89wISn9Ln+7dtgxj2DSKqB1TuAMM9MU0NUnq0iikA4VVK5+swSP36YSmNsBKlGl1K2yCMA7WJK8z3ArEQpvmfyUeOZ5C8WRg33OMcmq4+sPxD5CtykAC+7WPTpmckRvOhNn2oKmrEdNvMaeQAyOSMc9z1J5BJCmSlqwPVbSlXIlVFjC1XkJdeOSMrAWuyIr8tamFzXGHw8vc8l2ka5wmRIKDQGJ1oDhj3qH8/fdkeyaqGnXrXz8sCEdRle1vaZE/ecNgXVoNlemjy08BmhlD4snajks2xXgofucVibCJPgxmBHeJXTu09Rw1/lcxXiOXp1vtnycQGmRCPR39nHrwpA6VUDoVz3xakvlyZEcN6kBMiw0Ffsr2JG5mBzLULJ+gjHNuvdVxjUqLtJw/vyRd110g+zF1/xgWBQAh8aQMnhWpI3diRdJz8ef8IurQG6t6DGQ9S+8pfdYSJlbCGJeqs5FhH2JRUjWZXXrnExCWyiXFlfiJDjMEtl5g8UtW0OHCU4fG6Wl8pNwcMYqT4AaH8szsfMKjOVIVenYF9ppyjz09BrKA7VQJ5ZwqqMwH1bUanoMWxvZ+QglTbg+BICqozVZiKMwo6qzEqNSt32ovjfCYTgx131bWix4uC9IWyKNGbro3cOnN3OXNU9c3F6U9hG99cMWMzZhwpKXyiXknjJQDAQHnu/HKTCnruZBT/055+TpciEXb2fw== -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 |
18 | 19 | 20 | 21 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /java基础/Lambda/Lambda在List中的简单用法.md: -------------------------------------------------------------------------------- 1 | Lambda在`jdk1.8`里面已经很好用了,在这里不讲底层的实现,只有简单的用法,会继续补全。 2 | 首先一个list我们要使用`lambda`的话,需要使用它的`stream()`方法,获取流,才能使用后续的方法。 3 | #### 基础类User.java 4 | ```java 5 | public class User { 6 | 7 | public long userId; 8 | 9 | public User() { 10 | } 11 | 12 | public User(long userId, String name, int age) { 13 | this.userId = userId; 14 | this.name = name; 15 | this.age = age; 16 | } 17 | 18 | public String name; 19 | public int age; 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | public void setName(String name) { 26 | this.name = name; 27 | } 28 | 29 | public int getAge() { 30 | return age; 31 | } 32 | 33 | public void setAge(int age) { 34 | this.age = age; 35 | } 36 | 37 | public long getUserId() { 38 | return userId; 39 | } 40 | 41 | public void setUserId(long userId) { 42 | this.userId = userId; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "User{" + 48 | "name='" + name + '\'' + 49 | ", age=" + age + 50 | ", userId=" + userId + 51 | '}'; 52 | } 53 | 54 | public void output() { 55 | System.out.println("User{" + 56 | "name='" + name + '\'' + 57 | ", age=" + age + 58 | ", userId=" + userId + 59 | '}'); 60 | } 61 | } 62 | ``` 63 | 64 | #### 1.遍历元素 65 | 使用foreach方法,其中`s->`里面的s指list里面的每一个元素,针对每一个元素都执行后续的方法。如果里面只有一句话,可以直接缩写`foreach(n -> System.out.println(n));`,如果需要执行的方法里面有两句或者多句需要执行的话,需要可以使用`list.stream().forEach(s -> {System.out.println(s);});`形式。 66 | ```java 67 | // 遍历list(String)和对象 68 | public static void foreachListString() { 69 | List features = Arrays.asList("Lambdas", "Default Method", "Stream API", 70 | "Date and Time API"); 71 | features.forEach(n -> System.out.println(n)); 72 | List list = new ArrayList<>(); 73 | list.add("a"); 74 | list.add("b"); 75 | list.add("c"); 76 | // s代表的是里面的每一个元素,{}里面就是每个元素执行的方法,这个比较容易理解 77 | list.stream().forEach(s -> { 78 | System.out.println(s); 79 | }); 80 | 81 | // 处理对象 82 | List users = new ArrayList<>(); 83 | User user1 = new User(); 84 | user1.setAge(1); 85 | user1.setName("user1"); 86 | user1.setUserId(1); 87 | users.add(user1); 88 | users.stream().forEach(s -> s.output()); 89 | } 90 | ``` 91 | #### 2.转化里面的每一个元素 92 | map是需要返回值的,s代表里面的每一个元素,return 处理后的返回值 93 | ```java 94 | public static void mapList() { 95 | List list = new ArrayList<>(); 96 | list.add("a"); 97 | list.add("b"); 98 | list.add("c"); 99 | List list2 = new ArrayList<>(); 100 | // map代表从一个转成另一个,s代表里面的每一个值,{}代表针对每一个值的处理方法,如果是代码句子,则需要有返回值 101 | // 返回值代表转化后的值,以下两种都可以 102 | list2 = list.stream().map(s -> { 103 | return s.toUpperCase(); 104 | }).collect(Collectors.toList()); 105 | list2.stream().forEach(s -> { 106 | System.out.println(s); 107 | }); 108 | list2 = list.stream().map(s -> s.toUpperCase()).collect(Collectors.toList()); 109 | list2.stream().forEach(s -> { 110 | System.out.println(s); 111 | }); 112 | } 113 | ``` 114 | 115 | #### 3.条件过滤筛选 116 | 使用filter函数,里面的表达式也是需要返回值的,返回值应该为boolean类型,也就是符合条件的就保留在list里面,不符合条件的就被过滤掉。 117 | 118 | ```java 119 | // filter过滤 120 | public static void filterList() { 121 | List list1 = new ArrayList<>(); 122 | List list2 = new ArrayList<>(); 123 | list1.add("aasd"); 124 | list1.add("agdfs"); 125 | list1.add("bdfh"); 126 | list2 = list1.stream().filter(s -> { 127 | return s.contains("a"); 128 | }).collect(Collectors.toList()); 129 | list2.stream().forEach(s -> { 130 | System.out.println(s); 131 | }); 132 | } 133 | ``` 134 | 135 | #### 4.取出list里面的对象中的元素,返回一个特定的list 136 | 这个可以让我们取出list集合中的某一个元素,也是使用map即可。 137 | ```java 138 | // list集合中取出某一属性 139 | public static void getAttributeList() { 140 | List list = new ArrayList<>(); 141 | User user1 = new User(); 142 | user1.setUserId(1); 143 | user1.setName("James"); 144 | user1.setAge(13); 145 | list.add(user1); 146 | User user2 = new User(); 147 | user2.setUserId(2); 148 | user2.setName("Tom"); 149 | user2.setAge(21); 150 | list.add(user2); 151 | // 两种书写方式都可以,一个是map里面,使用每一个实例调用User类的getName方法返回值就是转化后的值。 152 | List tableNames = list.stream().map(User::getName).collect(Collectors.toList()); 153 | tableNames.stream().forEach(s -> { 154 | System.out.println(s); 155 | }); 156 | List tableNames1 = list.stream().map(u -> u.getName()).collect(Collectors.toList()); 157 | tableNames1.stream().forEach(s -> { 158 | System.out.println(s); 159 | }); 160 | } 161 | ``` 162 | 163 | #### 5.分组 164 | 165 | 可以根据某一个属性来分组,获得`map` 166 | 167 | ``` java 168 | // 分组,每一组都是list 169 | public static void groupBy() { 170 | List userList = new ArrayList<>();// 存放user对象集合 171 | User user1 = new User(1, "张三", 24); 172 | User user2 = new User(2, "李四", 27); 173 | User user3 = new User(3, "王五", 21); 174 | User user4 = new User(4, "张三", 22); 175 | User user5 = new User(5, "李四", 20); 176 | User user6 = new User(6, "王五", 28); 177 | userList.add(user1); 178 | userList.add(user2); 179 | userList.add(user3); 180 | userList.add(user4); 181 | userList.add(user5); 182 | userList.add(user6); 183 | //根据name来将userList分组 184 | Map> groupBy = userList.stream().collect(Collectors.groupingBy(User::getName)); 185 | System.out.println(groupBy); 186 | } 187 | ``` 188 | 189 | #### 6.对某一个属性进行求和 190 | 比如我们需要对年龄进行求和,可以使用mapToInt(),里面参数应该使用`类名:方法名`,最后需要使用sum()来求和。 191 | ```java 192 | public static void getSum(){ 193 | List userList = new ArrayList<>();//存放user对象集合 194 | 195 | User user1 = new User(1, "qw", 24); 196 | User user2 = new User(2, "qwe", 27); 197 | User user3 = new User(3, "aasf", 21); 198 | User user4 = new User(4, "fa", 22); 199 | User user5 = new User(5, "sd", 20); 200 | User user6 = new User(6, "yr", 28); 201 | 202 | userList.add(user1); 203 | userList.add(user2); 204 | userList.add(user3); 205 | userList.add(user4); 206 | userList.add(user5); 207 | userList.add(user6); 208 | // sum()方法,则是对每一个元素进行加和计算 209 | int totalAge = userList.stream().mapToInt(User::getAge).sum(); 210 | System.out.println("和:" + totalAge); 211 | } 212 | ``` 213 | #### 7.将list转化成map 214 | 比如我们需要list里面的对象的id和这个对象对应,那就是需要转换成map。需要在collect()方法里面使用Collectors的toMap()方法即可,参数就是key和value。 215 | 216 | ```java 217 | public static void listToMap(){ 218 | List userList = new ArrayList<>(); 219 | 220 | User user1 = new User(1, "12", 22); 221 | User user2 = new User(2, "21", 17); 222 | User user3 = new User(3, "a", 11); 223 | User user4 = new User(4, "a", 22); 224 | User user5 = new User(5, "af", 22); 225 | User user6 = new User(6, "fa", 25); 226 | 227 | userList.add(user1); 228 | userList.add(user2); 229 | userList.add(user3); 230 | userList.add(user4); 231 | userList.add(user5); 232 | userList.add(user6); 233 | 234 | Map userMap = userList.stream().collect(Collectors.toMap(User::getUserId, user -> user)); 235 | System.out.println("toMap:" + userMap.toString()); 236 | } 237 | ``` 238 | 239 | **此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~** 240 | 241 | **技术之路不在一时,山高水长,纵使缓慢,驰而不息。** 242 | 243 | **公众号:秦怀杂货店** 244 | 245 | ![](https://img-blog.csdnimg.cn/img_convert/7d98fb66172951a2f1266498e004e830.png) -------------------------------------------------------------------------------- /java基础/instanceof用法详解.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 1. instanceof关键字 4 | 5 | 如果你之前一直没有怎么仔细了解过`instanceof`关键字,现在就来了解一下: 6 | 7 | 8 | 9 | `instanceof`其实是java的一个二元操作符,和`=`,`<`,`>`这些是类似的,同时它也是被保留的关键字,主要的作用,是为了测试左边的对象,是不是右边的类的实例,返回的是boolean值。 10 | 11 | ```java 12 | A instanceof B 13 | ``` 14 | 注意:`A`是实例,而`B`则是`Class类` 15 | 16 | 下面使用代码测试一下: 17 | 18 | ```java 19 | class A{ 20 | } 21 | interface InterfaceA{ 22 | 23 | } 24 | class B extends A implements InterfaceA{ 25 | 26 | } 27 | public class Test { 28 | public static void main(String[] args) { 29 | B b = new B(); 30 | System.out.println(b instanceof B); 31 | System.out.println(b instanceof A); 32 | System.out.println(b instanceof InterfaceA); 33 | 34 | A a = new A(); 35 | System.out.println(a instanceof InterfaceA); 36 | } 37 | } 38 | ``` 39 | 40 | 41 | 42 | 输出结果如下: 43 | 44 | ```java 45 | true 46 | true 47 | true 48 | false 49 | ``` 50 | 51 | 从上面的结果,其实我们可以看出`instanceof`,相当于判断当前**对象**能不能装换成为该类型,`java`里面上转型是安全的,子类对象可以转换成为父类对象,接口实现类对象可以装换成为接口对象。 52 | 53 | 对象`a`和`Interface`没有什么关系,所以返回`false`。 54 | 55 | 56 | 57 | 那如果我们装换成为Object了,它还能认出来是哪一个类的对象么? 58 | 59 | ```java 60 | public class Test { 61 | public static void main(String[] args) { 62 | Object o = new ArrayList(); 63 | System.out.println(o instanceof ArrayList); 64 | 65 | String str = "hello world"; 66 | System.out.println(str instanceof String); 67 | System.out.println(str instanceof Object); 68 | } 69 | } 70 | ``` 71 | 72 | 上面的结果返回都是`true`,也就是认出来还是哪一个类的对象。同时我们使用`String`对象测试的时候,发现对象既是`String`的实例,也是`Object`的实例,也印证了`Java`里面所有类型都默认继承了`Obejct`。 73 | 74 | 75 | 76 | 但是值得注意的是,我们只能使用对象来`instanceof`,不能使用基本数据类型,否则会报错。 77 | 78 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201129204933.png) 79 | 80 | 81 | 82 | 如果对象为`null`,那是什么类型? 83 | 84 | 这个答案是:不知道什么类型,因为`null`可以转换成为任何类型,所以不属于任何类型,`instanceof`结果会是`false`。 85 | 86 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201129205213.png) 87 | 88 | 89 | 90 | 具体的实现策略我们可以在官网找到:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.instanceof 91 | 92 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201129205647.png) 93 | 94 | 如果`S`是`objectref`所引用的对象的类,而`T`是已解析类,数组或接口的类型,则`instanceof`确定是否 `objectref`是`T`的一个实例。`S s = new A(); s instanceof T` 95 | 96 | - 如果S是一个普通的(非数组)类,则: 97 | - 如果T是一个类类型,那么S必须是T的同一个类,或者S必须是T的子类; 98 | - 如果T是接口类型,那么S必须实现接口T。 99 | 100 | - 如果S是接口类型,则: 101 | - 如果T是类类型,那么T必须是Object。 102 | - 如果T是接口类型,那么T一定是与S相同的接口或S的超接口。 103 | 104 | - 如果S是表示数组类型SC的类[],即类型SC的组件数组,则: 105 | - 如果T是类类型,那么T必须是Object。 106 | - 如果T是一种接口类型,那么T必须是数组实现的接口之一(JLS§4.10.3)。 107 | - 如果T是一个类型为TC的数组[],即一个类型为TC的组件数组,那么下列其中一个必须为真: 108 | - TC和SC是相同的原始类型。 109 | - TC和SC是引用类型,类型SC可以通过这些运行时规则转换为TC。 110 | 111 | 112 | 113 | 114 | 115 | 但是具体的底层原理我在知乎找到的**R大** 回答的相关问题,https://www.zhihu.com/question/21574535,看完觉得我太弱了...我是菜鸟...我确实是菜鸟 116 | 117 | 118 | 119 | # 2. isInstance()方法 120 | 121 | 其实这个和上面那个是基本相同的,主要是这个调用者是`Class`对象,判断参数里面的对象是不是这个`Class`对象的实例。 122 | 123 | ```java 124 | class A { 125 | } 126 | 127 | interface InterfaceA { 128 | 129 | } 130 | 131 | class B extends A implements InterfaceA { 132 | 133 | } 134 | 135 | public class Test { 136 | public static void main(String[] args) { 137 | B b = new B(); 138 | System.out.println(B.class.isInstance(b)); 139 | System.out.println(A.class.isInstance(b)); 140 | System.out.println(InterfaceA.class.isInstance(b)); 141 | 142 | A a = new A(); 143 | System.out.println(InterfaceA.class.isInstance(a)); 144 | } 145 | } 146 | ``` 147 | 148 | 历史总是惊人的相似!!! 149 | 150 | ```java 151 | true 152 | true 153 | true 154 | false 155 | ``` 156 | 157 | 事实证明,这个`isInstance(o)`判断的是`o`是否属于当前`Class`类的实例. 158 | 159 | 不信?再来测试一下: 160 | 161 | ```java 162 | public class Test { 163 | public static void main(String[] args) { 164 | String s = "hello"; 165 | System.out.println(String.class.isInstance(s)); // true 166 | System.out.println(Object.class.isInstance(s)); // true 167 | 168 | 169 | System.out.println("============================="); 170 | Object o = new ArrayList(); 171 | System.out.println(String.class.isInstance(o)); // false 172 | System.out.println(ArrayList.class.isInstance(o)); // true 173 | System.out.println(Object.class.isInstance(o)); // true 174 | } 175 | } 176 | ``` 177 | 178 | 可以看出,其实就是装换成为`Object`,之前的类型信息还是会保留着,结果和`instance`一样,区别是: 179 | 180 | - `instanceof` :前面是实例对象,后面是类型 181 | - `isInstance`:调用者(前面)是类型对象,参数(后面)是实例对象 182 | 183 | 184 | 185 | 但是有一个区别哦😯,`isInstance()`这个方法,是可以使用在基本类型上的,其实也不算是用在基本类型,而是自动做了装箱操作。看下面👇: 186 | 187 | ```java 188 | System.out.println(Integer.class.isInstance(1)); 189 | ``` 190 | 191 | 参数里面的1,其实会被装箱成为`new Integer(1)`,所以这样用不会报错。 192 | 193 | 194 | 195 | # 3. instanceof,isInstance,isAssignableFrom区别是什么? 196 | 197 | - `instanceof` 判断对象和类型之间的关系,是关键字,只能用于对象实例,判断左边的对象是不是右边的类(包括父类)或者接口(包括父类)的实例化。 198 | - `isInstance(Object o)`:判断对象和类型之间的关系,判断`o`是不是调用这个方法的`class`(包括父类)或者接口(包括父类)的实例化。 199 | - `isAssignableFrom`:判断的是类和类之间的关系,调用者是否可以由参数中的`Class`对象转换而来。 200 | 201 | 202 | 203 | 注意:`java`里面一切皆是对象,所以,`class`本身也是对象。 204 | 205 | **【作者简介】**: 206 | 秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。 207 | 208 | 此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~ 209 | 210 | 211 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201012000828.png) 212 | 213 | -------------------------------------------------------------------------------- /java基础/isAssignableFrom的用法详解.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 最近在java的源代码中总是可以看到`isAssignableFrom()`这个方法,到底是干嘛的?怎么用? 3 | 4 | # 1. isAssignableFrom()是干什么用的? 5 | 首先我们必须知道的是,java里面一切皆对象,类本身也是会当成对象来处理,主要体现在类的`.class`文件,其实加载到java虚拟机之后,也是一个对象,它就是`Class`对象,全限定类名:`java.lang.Class`。 6 | 7 | 那这个`isAssignableFrom()`其实就是Class对象的一个方法: 8 | ```java 9 | /** 10 | * Determines if the class or interface represented by this 11 | * {@code Class} object is either the same as, or is a superclass or 12 | * superinterface of, the class or interface represented by the specified 13 | * {@code Class} parameter. It returns {@code true} if so; 14 | * otherwise it returns {@code false}. If this {@code Class} 15 | * object represents a primitive type, this method returns 16 | * {@code true} if the specified {@code Class} parameter is 17 | * exactly this {@code Class} object; otherwise it returns 18 | * {@code false}. 19 | * 20 | *

Specifically, this method tests whether the type represented by the 21 | * specified {@code Class} parameter can be converted to the type 22 | * represented by this {@code Class} object via an identity conversion 23 | * or via a widening reference conversion. See The Java Language 24 | * Specification, sections 5.1.1 and 5.1.4 , for details. 25 | * 26 | * @param cls the {@code Class} object to be checked 27 | * @return the {@code boolean} value indicating whether objects of the 28 | * type {@code cls} can be assigned to objects of this class 29 | * @exception NullPointerException if the specified Class parameter is 30 | * null. 31 | * @since JDK1.1 32 | */ 33 | public native boolean isAssignableFrom(Class cls); 34 | ``` 35 | 用`native`关键字描述,说明是一个底层方法,实际上是使用c/c++实现的,java里面没有实现,那么这个方法是干什么的呢?我们从上面的注释可以解读: 36 | 37 | 如果是`A.isAssignableFrom(B)` 38 | 确定一个类(B)是不是继承来自于另一个父类(A),一个接口(A)是不是实现了另外一个接口(B),或者两个类相同。主要,这里比较的维度不是实例对象,而是类本身,因为这个方法本身就是`Class`类的方法,判断的肯定是和类信息相关的。 39 | 40 | 也就是判断当前的Class对象所表示的类,是不是参数中传递的Class对象所表示的类的父类,超接口,或者是相同的类型。是则返回true,否则返回false。 41 | # 2.代码实验测试 42 | ## 2.1 父子继承关系测试 43 | ```java 44 | class A{ 45 | } 46 | class B extends A{ 47 | } 48 | class C extends B{ 49 | } 50 | public class test { 51 | public static void main(String[] args) { 52 | A a = new A(); 53 | B b = new B(); 54 | B b1 = new B(); 55 | C c = new C(); 56 | System.out.println(a.getClass().isAssignableFrom(a.getClass())); 57 | System.out.println(a.getClass().isAssignableFrom(b.getClass())); 58 | System.out.println(a.getClass().isAssignableFrom(c.getClass())); 59 | System.out.println(b1.getClass().isAssignableFrom(b.getClass())); 60 | 61 | System.out.println(b.getClass().isAssignableFrom(c.getClass())); 62 | 63 | System.out.println("====================================="); 64 | System.out.println(A.class.isAssignableFrom(a.getClass())); 65 | System.out.println(A.class.isAssignableFrom(b.getClass())); 66 | System.out.println(A.class.isAssignableFrom(c.getClass())); 67 | 68 | System.out.println("====================================="); 69 | System.out.println(Object.class.isAssignableFrom(a.getClass())); 70 | System.out.println(Object.class.isAssignableFrom(String.class)); 71 | System.out.println(String.class.isAssignableFrom(Object.class)); 72 | } 73 | } 74 | ``` 75 | 运行结果如下: 76 | ``` java 77 | true 78 | true 79 | true 80 | true 81 | true 82 | ===================================== 83 | true 84 | true 85 | true 86 | ===================================== 87 | true 88 | true 89 | false 90 | ``` 91 | 92 | 从运行结果来看,`C`继承于`B`,`B`继承于`A`,那么任何一个类都可以`isAssignableFrom`其本身,这个从中文意思来理解就是可以从哪一个装换而来,自身装换而来肯定是没有问题的,父类可以由子类装换而来也是没有问题的,所以A可以由B装换而来,同时也可以由子类的子类转换而来。 93 | 94 | 上面的代码也说明一点,所有的类,其最顶级的父类也是`Object`,也就是所有的类型都可以转换成为`Object`。 95 | 96 | ## 2.2 接口的实现关系测试 97 | ```java 98 | interface InterfaceA{ 99 | } 100 | 101 | class ClassB implements InterfaceA{ 102 | 103 | } 104 | class ClassC implements InterfaceA{ 105 | 106 | } 107 | class ClassD extends ClassB{ 108 | 109 | } 110 | public class InterfaceTest { 111 | public static void main(String[] args) { 112 | System.out.println(InterfaceA.class.isAssignableFrom(InterfaceA.class)); 113 | System.out.println(InterfaceA.class.isAssignableFrom(ClassB.class)); 114 | System.out.println(InterfaceA.class.isAssignableFrom(ClassC.class)); 115 | System.out.println(ClassB.class.isAssignableFrom(ClassC.class)); 116 | System.out.println("============================================"); 117 | 118 | System.out.println(ClassB.class.isAssignableFrom(ClassD.class)); 119 | System.out.println(InterfaceA.class.isAssignableFrom(ClassD.class)); 120 | } 121 | } 122 | ``` 123 | 124 | 输出结果如下: 125 | ```java 126 | true 127 | true 128 | true 129 | false 130 | ============================================ 131 | true 132 | true 133 | ``` 134 | 135 | 从上面的结果看,其实接口的实现关系和类的实现关系是一样的,没有什么区别,但是如果`B`和`C`都实现了同一个接口,他们之间其实是不能互转的。 136 | 137 | 如果`B`实现了接口`A`,`D`继承了`B`,实际上`D`是可以上转为A接口的,相当于`D`间接实现了`A`,这里也说明了一点,其实继承关系和接口实现关系,在`isAssignableFrom()`的时候是一样的,一视同仁的。 138 | 139 | 140 | # 3.总结 141 | `isAssignableFrom`是用来判断子类和父类的关系的,或者接口的实现类和接口的关系的,默认所有的类的终极父类都是`Object`。如果`A.isAssignableFrom(B)`结果是true,证明`B`可以转换成为`A`,也就是`A`可以由`B`转换而来。 142 | 143 | 这个方法在我们平时使用的不多,但是很多源码里面判断两个类之间的关系的时候,**(注意:是两个类的关系,不是两个类的实例对象的关系!!!)**,会使用到这个方法来判断,大概因为框架代码或者底层代码都是经过多层抽象,做到容易拓展和解耦合,只能如此。 144 | 145 | **【作者简介】**: 146 | 秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。 147 | 148 | 此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~ 149 | 150 | 151 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201012000828.png) 152 | -------------------------------------------------------------------------------- /java基础/java native关键字.md: -------------------------------------------------------------------------------- 1 | 2 | [TOC] 3 | 今天一不小心跟进Object的源码中,发现一个`native`关键字,一脸蒙蔽,怎么我从来没有用过。 4 | ``` java 5 | // 这是计算对象的hsahcode的方法,涉及到内存地址 6 | public native int hashCode(); 7 | ``` 8 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201118222529.png) 9 | 10 | 1.汇编生`c`,`c`生万物,其实java要实现对底层的控制,还是需要`c/c++`帮忙,老大毕竟是老大。 11 | 12 | 2.`native`关键字我们开发应用的时候是用不到的,那什么时候用到呢?那些开发**java语言**的时候用到,`native`关键字是与`c++`联合开发的时候使用的,要不java控制不了底层啊,比如内存。所以还是那句:汇编生`c`,`c`生万物,`c++`是`c`的升级版。 13 | 14 | 3.这是`java`调用其他地方的接口的一个声明关键字,意思是这个方法不是java实现的,有挺多的编程语言都有这样的特性,比如`c++`里面使用`extern "c"`来表示告诉c++编译器去调用c里面已经实现好的函数,而不是自己去实现。`native`方法有点像`java` 里面的`interface`,都不用去实现,而是有别人去实现,但是`interface`是谁实现接口谁实现,`native`方法是直接交给`c/c++`来实现。`java`只能调用,由操作系统实现。 15 | 16 | 4.`native`方法不能与`abstract`方法一起使用,因为`native`表示这些方法是有实现体的,但是`abstract`却表示这些方法是没有实现体的,那么两者矛盾,肯定也不能一起使用。 17 | 18 | 19 | ## 1.怎么调用到native方法的呢? 20 | 上面说`native`表示这个方法不是`java`实现的,那么就不是原生态方法,也就不会存在这个文件中,而是存在其他地方,那么java要怎么调用才能调用到呢? 21 | 22 | > * JNI(Java Native Interface)这是一个本机编程的接口,它也是java jdk(开发工具包)的一部分,JNI可以支持java中使用其他语言,java要调用其他语言的接口,需要经过他处理。java所谓的跨平台,在一定程度上放弃了底层操作,因为不同的硬件或者操作系统底层的操作都是不一样的。 23 | 24 | ![](https://img-blog.csdnimg.cn/img_convert/31426e38350ffdfdc9bd09b72d13e877.png) 25 | 26 | 那么我们现在来写一个程序:`helloWorld.java`(我的所有写的文件都放在桌面,同个文件夹即可) 27 | 28 | ``` java 29 | public class helloworld{ 30 | static 31 | { 32 | System.loadLibrary("cSayHello"); 33 | } 34 | public static native void hello(); 35 | @SuppressWarnings("static-access") 36 | public static void main(String[] args){ 37 | new helloworld().hello(); 38 | } 39 | } 40 | ``` 41 | 直接在编译器运行这段代码会出现下面错误: 42 | 43 | ![](https://img-blog.csdnimg.cn/img_convert/9dee6cd8edb7a01ef8ebbded3d949a3e.png) 44 | 45 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201118223648.png) 46 | 47 | 上面的错误是说找不到`cSayHello`:`no cSayHello in java.library.path`,所以啊,这个`c/c++`的方法我们要自己实现,毕竟我们用的不是操作系统以及定义好的方法。 48 | 所以我们先来,使用cmd **在helloworld.java所在的目录下** 使用命令行: 49 | ``` shell 50 | javac helloworld 51 | javah helloworld 52 | ``` 53 | ![](https://img-blog.csdnimg.cn/img_convert/db0557017bfcf65baf30b09cdfe13529.png) 54 | 55 | 然后我们可以看到**在helloworld.java所在的目录下**多了两个文件,一个是**helloworld.class**文件,一个是**helloworld.h**文件。 56 | 57 | 打开**helloworld.h**,里面引用了**jni.h**这个文件,这个文件在我们安装的`java`目录下面的`include`文件下: 58 | 59 | ```java 60 | /* DO NOT EDIT THIS FILE - it is machine generated */ 61 | #include 62 | /* Header for class helloworld */ 63 | 64 | #ifndef _Included_helloworld 65 | #define _Included_helloworld 66 | #ifdef __cplusplus 67 | extern "C" { 68 | #endif 69 | /* 70 | * Class: helloworld 71 | * Method: hello 72 | * Signature: ()V 73 | */ 74 | JNIEXPORT void JNICALL Java_helloworld_hello 75 | (JNIEnv *, jclass); 76 | 77 | #ifdef __cplusplus 78 | } 79 | #endif 80 | #endif 81 | ``` 82 | 83 | 我的`java`是装在`D盘`下面: 84 | 85 | ![](https://img-blog.csdnimg.cn/img_convert/c4244e20fac830111b6b8c9dcc12e6c2.png) 86 | 87 | 我们来写需要引入的`c`文件`cSayHello`,我也是放在桌面,反正同一个文件夹就可以。 88 | ``` C++ 89 | #include "helloworld.h" 90 | #include 91 | 92 | JNIEXPORT void JNICALL Java_helloworld_hello(JNIEnv *env, jclass jc) 93 | { 94 | printf("java helloworld"); 95 | } 96 | ``` 97 | 在`windows`系统上,需要下载安装`WinGW Gcc`,安装教程参考https://www.jianshu.com/p/535a3131ccd8, 安装成功`cmd`输入: 98 | ``` shell 99 | gcc -m64 -Wl,--add-stdcall-alias -I"D:\Java\jdk1.8.0_111\include" -I"D:\Java\jdk1.8.0_111\include\win32" -shared -o cSayHello.dll helloworld.c 100 | ``` 101 | 然后直接运行,就可以看到输出了 102 | ``` java 103 | java helloworld 104 | ``` 105 | 106 | ## 2. java调用自定义native方法步骤 107 | 在java中使用native的步骤: 108 | 1.在java代码中声明native方法 109 | 2.执行javah来生成一个.h文件 110 | 3.写.cpp文件来实现native导出的方法,需要包含上面第二步产生的.h文件,同时也包含了jdk自带的jni.h 111 | 4.将第三步的.cpp文件通过gcc 编译成动态链接库文件 112 | 5.在java中使用的用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问 113 | 一般情况下,我们jdk中声明的native方法,在编译的时候都会自动去加载动态链接库文件,而不需要我们自己去操作了。 114 | 115 | ## 3.使用native的缺点 116 | 使用native的缺点:可移植性差,把对底层的控制权交给其他语言,那么也会出现不稳定性,庆幸的是现在操作系统的底层实现基本不会改变。上面hsahcode()的计算真是通过内存所在的内存块来计算的,java是无法直接操作内存的。 117 | 118 | 119 | **【作者简介】**: 120 | 秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。 121 | 122 | 此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~ 123 | 124 | 125 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201012000828.png) 126 | -------------------------------------------------------------------------------- /java基础/java接口与抽象类异同分析.md: -------------------------------------------------------------------------------- 1 | 2 | > * 在java中,通常初学者搞不懂接口与抽象类,这也是面试比较容易问到的一个问题。下面我来谈谈自己的理解。如有不妥之处,还望批评指正,不胜感激。 3 | 4 | [TOC] 5 | ## 1.抽象类怎么定义和继承? 6 | 我们定义一个抽象类`person.class`表示类(人): 7 | ```java 8 | //使用关键字abstract 9 | public abstract class person { 10 | //吃东西的抽象方法,已经有所实现 11 | public void eat(){ 12 | System.out.println("我是抽象方法吃东西"); 13 | } 14 | 15 | //public 修饰的空实现的方法 16 | public void run(){} 17 | 18 | //无修饰,空实现 19 | void walk(){} 20 | 21 | //protected修饰的方法,空实现 22 | protected void sleep(){} 23 | 24 | //private修饰的空实现方法 25 | private void read(){} 26 | } 27 | 28 | ``` 29 | 30 | - 1.抽象类使用abstract修饰,可以有抽象方法,也可以完全没有抽象方法,也可以是实现了的方法,但是所有的方法必须实现,空实现(`public void walk(){}`)也是实现的一种,而不能写 ~~`public void eat()`~~,后面必须带大括号。 31 | - 2.方法修饰符可以使`public`,`protected`,`private`,或者是没有,没有默认为只能在同一个包下面继承,如果是`private`那么子类继承的时候就无法继承这个方法,也没有办法进行修改. 32 | - 下面我们来写一个`Teacher.class`继承抽象类 33 | 34 | 35 | 同一个包下继承: 36 | ![](https://img-blog.csdnimg.cn/img_convert/bd3199b57749b6a290d6c21fd8a38da3.png) 37 | 38 | 不同的包下面继承: 39 | ![](https://img-blog.csdnimg.cn/img_convert/3791b40aff8c13be5ee211d029660513.png) 40 | 41 | 同个包下正确的代码如下(不重写私有的方法): 42 | ```java 43 | public class teacher extends person { 44 | @Override 45 | public void run(){ 46 | System.out.println("我是实体类的方法跑步"); 47 | } 48 | @Override 49 | void walk(){ 50 | System.out.println("我是实体类的方法走路"); 51 | } 52 | @Override 53 | protected void sleep(){ 54 | System.out.println("我是实体类的方法睡觉"); 55 | } 56 | } 57 | 58 | ``` 59 | - 结果如下(**没有覆盖抽象类吃东西的方法,所以会调用抽象类默认的**): 60 | 61 | ![](https://img-blog.csdnimg.cn/img_convert/5a34eda5ca7e887ef5a64ce42658f9e5.png) 62 | 63 | - 下面代码是重写了`eat()`方法的代码,重写是即使没有使用`@Override`也是起作用的: 64 | ```java 65 | public class teacher extends person { 66 | public void eat(){ 67 | System.out.println("我是实体类的方法吃东西"); 68 | } 69 | @Override 70 | public void run(){ 71 | System.out.println("我是实体类的方法跑步"); 72 | } 73 | @Override 74 | void walk(){ 75 | System.out.println("我是实体类的方法走路"); 76 | } 77 | @Override 78 | protected void sleep(){ 79 | System.out.println("我是实体类的方法睡觉"); 80 | } 81 | } 82 | ``` 83 | - 结果如下,吃东西的方法被覆盖掉了: 84 | ![](https://img-blog.csdnimg.cn/img_convert/ab654ad52453bf6a1e4a33d3dcc38f9b.png) 85 | 86 | - 抽象类不能被实例化,比如: 87 | ![](https://img-blog.csdnimg.cn/img_convert/f190470d45ceb45fa2941da283ddeb6f.png) 88 | 89 | - 子类可以实现抽象类的方法,也可以不实现,也可以只实现一部分,这样跑起来都是没有问题的,不实现的话,调用是默认使用抽象类的空实现,也就是什么都没有输出,要是抽象类有实现,那么会输出抽象类默认方法。 90 | 比如: 91 | 92 | ![](https://img-blog.csdnimg.cn/img_convert/70a809a1a123d2dfa51f16d7ff7043fd.png) 93 | 94 | ![](https://img-blog.csdnimg.cn/img_convert/7a8130d6994494b764946162f41a79e1.png) 95 | 96 | - 抽象类中可以有具体的方法以及属性(成员变量) 97 | - 抽象类和普通类之间有很多相同的地方,比如他们都可以都静态成员与静态代码块等等。 98 | 99 | ## 2.接口怎么定义和实现? 100 | > * 接口就是对方法或者动作的抽象,比如`person.class`想要成为教师,可以实现教师的接口,可以理解为增加能力。 101 | > * 接口不允许定义没有初始化的属性变量,可以定义`public static final int i=5;`,以及`public int number =0;`,但不允许`public int num;`这样定义,所有`private`的变量都不允许出现,下面是图片 102 | 103 | 104 | ![](https://img-blog.csdnimg.cn/img_convert/b4c8b2a8b04f17b68cb33c868f0455df.png) 105 | 106 | 107 | 定义`public int number =0;`默认是final修饰的,所以也不能改变它的值: 108 | ![](https://img-blog.csdnimg.cn/img_convert/74bb4bf81789e6856d977e747718b883.png) 109 | 110 | 下面是正确的接口代码:Teacher.java 111 | ```java 112 | public interface Teacher { 113 | public static final int i=5; 114 | public int number =0; 115 | public void teach(); 116 | void study(); 117 | } 118 | ``` 119 | - 实现类TeacherClass.java 120 | ```java 121 | public class TeacherClass implements Teacher{ 122 | @Override 123 | public void teach() { 124 | System.out.println("我是一名老师,我要教书"); 125 | System.out.println("接口的static int是:"+i); 126 | } 127 | 128 | @Override 129 | public void study() { 130 | System.out.println("我是一名老师,我也要学习"); 131 | System.out.println("接口的int number是:"+number); 132 | } 133 | } 134 | ``` 135 | - 测试类Test.java 136 | ```java 137 | public class Test { 138 | public static void main(String[] args){ 139 | TeacherClass teacherClass = new TeacherClass(); 140 | teacherClass.study(); 141 | teacherClass.teach(); 142 | System.out.println("-----------------------------------------------------"); 143 | Teacher teacher =teacherClass; 144 | teacher.study(); 145 | teacher.teach(); 146 | } 147 | } 148 | ``` 149 | 结果: 150 | ![](https://img-blog.csdnimg.cn/img_convert/ba958c533602f309ee63be73b4f76c8b.png) 151 | 152 | 分析:接口里面所定义的成员变量都是`final`的,不可变的,实现接口必须实现接口里面所有的方法,不能只实现一部分,没有使用`static final`修饰的,默认也是`final`,同时必须有初始化的值,接口不能直接创建对象,比如~~Teacher teacher = new Teacher()~~ ,但是可以先创建一个接口的实现类,然后再赋值于接口对象。 153 | 154 | ## 3.总结与对比 155 | 156 | 抽象类 | 接口 157 | ---|--- 158 | 使用关键字`abstract`修饰 | 使用关键字`interface` 159 | 使用关键字`extends`实现继承,可以只实现一部分方法,一部分不实现,或者不实现也可以 | `implements`来实现接口,实现接口必须实现里面都有的方法 160 | 抽象类里面的方法可以是空实现,可以默认实现,但是必须要带{} | 接口里面的方法都没有实现体,也就是{} 161 | 抽象类中可以有具体的方法以及属性,也可以有静态代码块,静态成员变量 | 接口里面不能有普通成员变量,必须都是不可变的`final`成员变量,而且所有的成员变量都必须是`public` 162 | 抽象类里面的方法可以是`public`,`protect`,`private`,但是`private`无法继承,所以很少人会这么写,如果没有修饰符,那么只能是同一个包下面的类才能继承 | 接口的方法只能是`public`或者无修饰符,所有的`private`修饰都是会报错的 163 | 如果有改动,添加新的方法,可以直接在抽象类中实现默认的即可,也可以在实现类中实现 | 接口增加新方法必须在接口中声明,然后在实现类中进行实现 164 | 抽象类不能直接创建对象| 接口也不能直接创建对象 ,可以赋予实现类的对象 165 | 抽象类可以有`main`方法,而且我们可以直接运行,抽象类也可以有构造器 | 接口不能有`main`方法,接口不能有构造器 166 | 167 | 那么我们什么时候使用接口什么时候使用抽象类呢? 168 | > * java有一个缺点,只能实现单继承,个人觉得接口是为了弥补单继承而设计的。 169 | > * 接口是对本质的抽象,比如人,可以设计为`person.class`这个抽象类,提供相关的方法,属性,但是接口是只提供方法的,也就是像增加功能的,那么也就是对方法的抽象。 170 | > * 如果需要默认实现,或者基本功能不断改变,那么建议使用抽象类,如果只是增加一种方法,那么建议使用接口,如果想实现多重继承,只能是接口与抽象类一起使用以达到想要实现的功能。 171 | 172 | 本文章是初学时的记录,仅是初级的对比,深入学习还需各位保持Keep going~ 173 | 174 | **此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~** 175 | 176 | **技术之路不在一时,山高水长,纵使缓慢,驰而不息。Keep going~** 177 | 178 | **公众号:秦怀杂货店** 179 | 180 | ![](https://img-blog.csdnimg.cn/img_convert/7d98fb66172951a2f1266498e004e830.png) 181 | -------------------------------------------------------------------------------- /java基础/java集合基础/11.ArrayList,Vector,LinkedList的区别是什么?.md: -------------------------------------------------------------------------------- 1 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210305104323.png) 2 | 3 | [TOC] 4 | 要想回答这个问题,可以先把各种都讲特性,然后再从底层存储结构,线程安全,默认大小,扩容机制,迭代器,增删改查效率这几个方向入手。 5 | 6 | ## 特性列举 7 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210306021250.png) 8 | 9 | - `ArrayList`:动态数组,使用的时候,只需要操作即可,内部已经实现扩容机制。 10 | - 线程不安全 11 | - 有顺序,会按照添加进去的顺序排好 12 | - 基于数组实现,随机访问速度快,插入和删除较慢一点 13 | - 可以插入`null`元素,且可以重复 14 | - `Vector`和前面说的`ArrayList`很是类似,这里说的也是1.8版本,它是一个队列,但是本质上底层也是数组实现的。同样继承`AbstractList`,实现了`List`,`RandomAcess`,`Cloneable`, `java.io.Serializable`接口。具有以下特点: 15 | - 提供随机访问的功能:实现`RandomAcess`接口,这个接口主要是为`List`提供快速访问的功能,也就是通过元素的索引,可以快速访问到。 16 | - 可克隆:实现了`Cloneable`接口 17 | - 是一个支持新增,删除,修改,查询,遍历等功能。 18 | - 可序列化和反序列化 19 | - 容量不够,可以触发自动扩容 20 | - **最大的特点是:线程安全的*,相当于线程安全的`ArrayList`。 21 | - LinkedList:链表结构,继承了`AbstractSequentialList`,实现了`List`,`Queue`,`Cloneable`,`Serializable`,既可以当成列表使用,也可以当成队列,堆栈使用。主要特点有: 22 | - 线程不安全,不同步,如果需要同步需要使用`List list = Collections.synchronizedList(new LinkedList());` 23 | - 实现`List`接口,可以对它进行队列操作 24 | - 实现`Queue`接口,可以当成堆栈或者双向队列使用 25 | - 实现Cloneable接口,可以被克隆,浅拷贝 26 | - 实现`Serializable`,可以被序列化和反序列化 27 | 28 | ## 底层存储结构不同 29 | `ArrayList`和`Vector`底层都是数组结构,而`LinkedList`在底层是双向链表结构。 30 | 31 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210305104801.png) 32 | 33 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210305104608.png) 34 | 35 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210305104903.png) 36 | 37 | ## 线程安全性不同 38 | ArrayList和LinkedList都不是线程安全的,但是Vector是线程安全的,其底层是用了大量的synchronized关键字,效率不是很高。 39 | 40 | 如果需要ArrayList和LinkedList是线程安全的,可以使用Collections类中的静态方法synchronizedList(),获取线程安全的容器。 41 | 42 | ## 默认的大小不同 43 | ArrayList如果我们创建的时候不指定大小,那么就会初始化一个默认大小为10,`DEFAULT_CAPACITY`就是默认大小。 44 | ```java 45 | private static final int DEFAULT_CAPACITY = 10; 46 | ``` 47 | 48 | Vector也一样,如果我们初始化,不传递容量大小,什么都不指定,默认给的容量是10: 49 | ``` java 50 | public Vector() { 51 | this(10); 52 | } 53 | ``` 54 | 55 | 而LinkedList底层是链表结构,是不连续的存储空间,没有默认的大小的说法。 56 | 57 | ## 扩容机制 58 | 59 | ArrayList和Vector底层都是使用数组`Object[]`来存储,当向集合中添加元素的时候,容量不够了,会触发扩容机制,ArrayList扩容后的容量是按照1.5倍扩容,而Vector默认是扩容2倍。两种扩容都是申请新的数组空间,然后调用数组复制的native函数,将数组复制过去。 60 | 61 | Vector可以设置每次扩容的增加容量,但是ArrayList不可以。Vector有一个参数capacityIncrement,如果capacityIncrement大于0,那么扩容后的容量,是以前的容量加上扩展系数,如果扩展系数小于等于0,那么,就是以前的容量的两倍。 62 | 63 | ## 迭代器 64 | `LinkedList`源码中一共定义了三个迭代器: 65 | - `Itr`:实现了`Iterator`接口,是`AbstractList.Itr`的优化版本。 66 | - `ListItr`:继承了`Itr`,实现了`ListIterator`,是`AbstractList.ListItr`优化版本。 67 | - `ArrayListSpliterator`:继承于`Spliterator`,Java 8 新增的迭代器,基于索引,二分的,懒加载器。 68 | 69 | `Vector`和`ArrayList`基本差不多,都是定义了三个迭代器: 70 | - `Itr`:实现接口`Iterator`,有简单的功能:判断是否有下一个元素,获取下一个元素,删除,遍历剩下的元素 71 | - `ListItr`:继承`Itr`,实现`ListIterator`,在`Itr`的基础上有了更加丰富的功能。 72 | - `VectorSpliterator`:可以分割的迭代器,主要是为了分割以适应并行处理。和`ArrayList`里面的`ArrayListSpliterator`类似。 73 | 74 | `LinkedList`里面定义了三种迭代器,都是以内部类的方式实现,分别是: 75 | - `ListItr`:列表的经典迭代器 76 | - `DescendingIterator`:倒序迭代器 77 | - `LLSpliterator`:可分割迭代器 78 | 79 | ## 增删改查的效率 80 | **理论上**,`ArrayList`和`Vector`检索元素,由于是数组,时间复杂度是`O(1)`,在集合的尾部插入或者删除是`O(1)`,但是其他的地方增加,删除,都是`O(n)`,因为涉及到了数组元素的移动。但是`LinkedList`不一样,`LinkedList`不管在任何位置,插入,删除都是`O(1)`的时间复杂度,但是`LinkedList`在查找的时候,是`O(n)`的复杂度,即使底层做了优化,可以从头部/尾部开始索引(根据下标在前一半还是后面一半)。 81 | 82 | 如果插入删除比较多,那么建议使用`LinkedList`,但是它并不是线程安全的,如果查找比较多,那么建议使用`ArrayList`,如果需要线程安全,先考虑使用`Collections`的`api`获取线程安全的容器,再考虑使用`Vector`。 83 | 84 | 测试三种结构在头部不断添加元素的结果: 85 | ```java 86 | 87 | import java.util.ArrayList; 88 | import java.util.LinkedList; 89 | import java.util.List; 90 | import java.util.Vector; 91 | 92 | public class Test { 93 | public static void main(String[] args) { 94 | addArrayList(); 95 | addLinkedList(); 96 | addVector(); 97 | } 98 | public static void addArrayList(){ 99 | List list = new ArrayList(); 100 | long startTime = System.nanoTime(); 101 | for(int i=0;i<100000;i++){ 102 | list.add(0,i); 103 | } 104 | long endTime = System.nanoTime(); 105 | System.out.println((endTime-startTime)/1000/60); 106 | } 107 | 108 | public static void addLinkedList(){ 109 | List list = new LinkedList(); 110 | long startTime = System.nanoTime(); 111 | for(int i=0;i<100000;i++){ 112 | list.add(0,i); 113 | } 114 | long endTime = System.nanoTime(); 115 | System.out.println((endTime-startTime)/1000/60); 116 | } 117 | public static void addVector(){ 118 | List list = new Vector(); 119 | long startTime = System.nanoTime(); 120 | for(int i=0;i<100000;i++){ 121 | list.add(0,i); 122 | } 123 | long endTime = System.nanoTime(); 124 | System.out.println((endTime-startTime)/1000/60); 125 | } 126 | } 127 | 128 | ``` 129 | 测出来的结果,LinkedList最小,Vector费时最多,基本验证了结果: 130 | ```txt 131 | ArrayList:7715 132 | LinkedList:111 133 | Vector:8106 134 | ``` 135 | 136 | 测试get的时间性能,往每一个里面初始化10w个数据,然后每次get出来: 137 | ```java 138 | 139 | import java.util.ArrayList; 140 | import java.util.LinkedList; 141 | import java.util.List; 142 | import java.util.Vector; 143 | 144 | public class Test { 145 | public static void main(String[] args) { 146 | getArrayList(); 147 | getLinkedList(); 148 | getVector(); 149 | } 150 | public static void getArrayList(){ 151 | List list = new ArrayList(); 152 | for(int i=0;i<100000;i++){ 153 | list.add(0,i); 154 | } 155 | long startTime = System.nanoTime(); 156 | for(int i=0;i<100000;i++){ 157 | list.get(i); 158 | } 159 | long endTime = System.nanoTime(); 160 | System.out.println((endTime-startTime)/1000/60); 161 | } 162 | 163 | public static void getLinkedList(){ 164 | List list = new LinkedList(); 165 | for(int i=0;i<100000;i++){ 166 | list.add(0,i); 167 | } 168 | long startTime = System.nanoTime(); 169 | for(int i=0;i<100000;i++){ 170 | list.get(i); 171 | } 172 | long endTime = System.nanoTime(); 173 | System.out.println((endTime-startTime)/1000/60); 174 | } 175 | public static void getVector(){ 176 | List list = new Vector(); 177 | for(int i=0;i<100000;i++){ 178 | list.add(0,i); 179 | } 180 | long startTime = System.nanoTime(); 181 | for(int i=0;i<100000;i++){ 182 | list.get(i); 183 | } 184 | long endTime = System.nanoTime(); 185 | System.out.println((endTime-startTime)/1000/60); 186 | } 187 | } 188 | ``` 189 | 190 | 测出来的时间如下,`LinkedList` 执行`get`操作确实耗时巨大,`Vector`和`ArrayList`在单线程环境其实差不多,多线程环境会比较明显,这里就不测试了: 191 | ``` txt 192 | ArrayList : 18 193 | LinkedList : 61480 194 | Vector : 21 195 | ``` 196 | 197 | 测试删除操作的代码如下,删除的时候我们是不断删除第0个元素: 198 | ```java 199 | import java.util.ArrayList; 200 | import java.util.LinkedList; 201 | import java.util.List; 202 | import java.util.Vector; 203 | 204 | public class Test { 205 | public static void main(String[] args) { 206 | removeArrayList(); 207 | removeLinkedList(); 208 | removeVector(); 209 | } 210 | public static void removeArrayList(){ 211 | List list = new ArrayList(); 212 | for(int i=0;i<100000;i++){ 213 | list.add(0,i); 214 | } 215 | long startTime = System.nanoTime(); 216 | for(int i=0;i<100000;i++){ 217 | list.remove(0); 218 | } 219 | long endTime = System.nanoTime(); 220 | System.out.println((endTime-startTime)/1000/60); 221 | } 222 | 223 | public static void removeLinkedList(){ 224 | List list = new LinkedList(); 225 | for(int i=0;i<100000;i++){ 226 | list.add(0,i); 227 | } 228 | long startTime = System.nanoTime(); 229 | for(int i=0;i<100000;i++){ 230 | list.remove(0); 231 | } 232 | long endTime = System.nanoTime(); 233 | System.out.println((endTime-startTime)/1000/60); 234 | } 235 | public static void removeVector(){ 236 | List list = new Vector(); 237 | for(int i=0;i<100000;i++){ 238 | list.add(i); 239 | } 240 | long startTime = System.nanoTime(); 241 | for(int i=0;i<100000;i++){ 242 | list.remove(0); 243 | } 244 | long endTime = System.nanoTime(); 245 | System.out.println((endTime-startTime)/1000/60); 246 | } 247 | } 248 | ``` 249 | 测试结果,LinkedList确实效率最高,但是`Vector`比`ArrayList`效率还要高。因为是单线程的环境,没有触发竞争的关系。 250 | ```txt 251 | ArrayList: 7177 252 | LinkedList: 34 253 | Vector: 6713 254 | ``` 255 | 256 | 下面来测试一下,vector多线程的环境,首先两个线程,每个删除5w元素: 257 | ```java 258 | package com.aphysia.offer; 259 | 260 | import java.util.ArrayList; 261 | import java.util.LinkedList; 262 | import java.util.List; 263 | import java.util.Vector; 264 | 265 | public class Test { 266 | public static void main(String[] args) { 267 | removeVector(); 268 | } 269 | 270 | public static void removeVector() { 271 | List list = new Vector(); 272 | for (int i = 0; i < 100000; i++) { 273 | list.add(i); 274 | } 275 | Thread thread1 = new Thread(new Runnable() { 276 | @Override 277 | public void run() { 278 | for (int i = 0; i < 50000; i++) { 279 | list.remove(0); 280 | } 281 | } 282 | }); 283 | Thread thread2 = new Thread(new Runnable() { 284 | @Override 285 | public void run() { 286 | for (int i = 0; i < 50000; i++) { 287 | list.remove(0); 288 | } 289 | } 290 | }); 291 | long startTime = System.nanoTime(); 292 | thread1.start(); 293 | thread2.start(); 294 | while (!list.isEmpty()) { 295 | 296 | } 297 | long endTime = System.nanoTime(); 298 | System.out.println((endTime - startTime) / 1000 / 60); 299 | } 300 | } 301 | ``` 302 | 测试时间为:12668 303 | 304 | 如果只使用一个线程,测试的时间是:8216,这也从结果说明了确实`Vector`在多线程的环境下,会竞争锁,导致执行时间变长。 305 | 306 | ## 总结一下 307 | 308 | - ArrayList 309 | - 底层是数组,扩容就是申请新的数组空间,复制 310 | - 线程不安全 311 | - 默认初始化容量是10,扩容是变成之前的1.5倍 312 | - 查询比较快 313 | - LinkedList 314 | - 底层是双向链表,可以往前或者往后遍历 315 | - 没有扩容的说法,可以当成双向队列使用 316 | - 增删比较快 317 | - 查找做了优化,index如果在前面一半,从前面开始遍历,index在后面一半,从后往前遍历。 318 | - Vector 319 | - 底层是数组,几乎所有方法都加了Synchronize 320 | - 线程安全 321 | - 有个扩容增长系数,如果不设置,默认是增加原来长度的一倍,设置则增长的大小为增长系数的大小。 -------------------------------------------------------------------------------- /java基础/java集合基础/3.iterable接口.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # iterable接口 4 | 整个接口框架关系如下(来自百度百科): 5 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/b3fb43166d224f4a5cebf37901f790529822d16e.jpg) 6 | 7 | `iterable`接口其实是java集合大家庭的最顶级的接口之一了,实现这个接口,可以视为拥有了获取迭代器的能力。`Iterable`接口出现在JDK1.5,那个时候只有`iterator()`方法,主要是定义了迭代集合内元素的规范。 8 | 实现了`Iterable`接口,我们可以使用增强的for循环,即 9 | ``` 10 | for(String str : lists){ 11 | System.out.println(str); 12 | } 13 | ``` 14 | 15 | ## 1. 内部定义的方法 16 | java集合最源头的接口,实现这个接口的作用主要是集合对象可以通过迭代器去遍历每一个元素。 17 | 18 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20200212223925.png) 19 | 20 | 源码如下: 21 | ``` java 22 | // 返回一个内部元素为T类型的迭代器(JDK1.5只有这个接口) 23 | Iterator iterator(); 24 | 25 | // 遍历内部元素,action意思为动作,指可以对每个元素进行操作(JDK1.8添加) 26 | default void forEach(Consumer action) {} 27 | 28 | // 创建并返回一个可分割迭代器(JDK1.8添加),分割的迭代器主要是提供可以并行遍历元素的迭代器,可以适应现在cpu多核的能力,加快速度。 29 | default Spliterator spliterator() { 30 | return Spliterators.spliteratorUnknownSize(iterator(), 0); 31 | } 32 | ``` 33 | 从上面可以看出,`foreach`迭代以及可分割迭代,都加了`default`关键字,这个是Java 8 新的关键字,以前接口的所有接口,具体子类都必须实现,而对于`deafult`关键字标识的方法,其子类可以不用实现,这也是接口规范发生变化的一点。 34 | 下面我们分别展示三个接口的调用: 35 | 36 | ### 1.1 iterator()方法 37 | `iterator()`方法,是接口中的核心方法,主要是获取迭代器,获取到的`iterator`有`next()`,`hasNext()`,`remove()`等方法。 38 | 39 | ```Java 40 | public static void iteratorHasNext(){ 41 | List list=new ArrayList(); 42 | list.add("Jam"); 43 | list.add("Jane"); 44 | list.add("Sam"); 45 | // 返回迭代器 46 | Iterator iterator=list.iterator(); 47 | // hashNext可以判断是否还有元素 48 | while(iterator.hasNext()){ 49 | //next()作用是返回当前指针指向的元素,之后将指针移向下个元素 50 | System.out.println(iterator.next()); 51 | } 52 | } 53 | ``` 54 | 当然也可以使用`for-each loop`方式遍历 55 | ```Java 56 | for (String item : list) { 57 | System.out.println(item); 58 | } 59 | ``` 60 | 但是实际上,这种写法在class文件中也是会转成迭代器形式,这只是一个语法糖。class文件如下: 61 | ```java 62 | public class IterableTest { 63 | public IterableTest() { } 64 | public static void main(String[] args) { 65 | iteratorHasNext(); 66 | } 67 | public static void iteratorHasNext() { 68 | List list = new ArrayList(); 69 | list.add("Jam"); 70 | list.add("Jane"); 71 | list.add("Sam"); 72 | Iterator iterator = list.iterator(); 73 | Iterator var2 = list.iterator(); 74 | while(var2.hasNext()) { 75 | String item = (String)var2.next(); 76 | System.out.println(item); 77 | } 78 | } 79 | } 80 | ``` 81 | 需要注意的一点是,迭代遍历的时候,如果删除或者添加元素,都会抛出修改异常,这是由于快速失败【fast-fail】机制。 82 | ```java 83 | public static void iteratorHasNext(){ 84 | List list=new ArrayList(); 85 | list.add("Jam"); 86 | list.add("Jane"); 87 | list.add("Sam"); 88 | for (String item : list) { 89 | if(item.equals("Jam")){ 90 | list.remove(item); 91 | } 92 | System.out.println(item); 93 | } 94 | } 95 | ``` 96 | 从下面的错误我们可以看出,第一个元素是有被打印出来的,也就是remove操作是成功的,只是遍历到第二个元素的时候,迭代器检查,发现被改变了,所以抛出了异常。 97 | ```java 98 | Jam 99 | Exception in thread "main" java.util.ConcurrentModificationException 100 | at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) 101 | at java.util.ArrayList$Itr.next(ArrayList.java:859) 102 | at IterableTest.iteratorHasNext(IterableTest.java:15) 103 | at IterableTest.main(IterableTest.java:7) 104 | ``` 105 | ### 1.2 forEach()方法 106 | 其实就是把对每一个元素的操作当成了一个对象传递进来,对每一个元素进行处理。 107 | ```java 108 | default void forEach(Consumer action) { 109 | Objects.requireNonNull(action); 110 | for (T t : this) { 111 | action.accept(t); 112 | } 113 | } 114 | ``` 115 | 当然像ArrayList自然也是有自己的实现的,那我们就可以使用这样的写法,简洁优雅。forEach方法在java8中参数是`java.util.function.Consumer`,可以称为**消费行为**或者说**动作**类型。 116 | ```java 117 | list.forEach(x -> System.out.print(x)); 118 | ``` 119 | 同时,我们只要实现`Consumer`接口,就可以自定义动作,如果不自定义,默认迭代顺序是按照元素的顺序。 120 | 121 | ```java 122 | public class ConsumerTest { 123 | public static void main(String[] args) { 124 | List list=new ArrayList(); 125 | list.add("Jam"); 126 | list.add("Jane"); 127 | list.add("Sam"); 128 | MyConsumer myConsumer = new MyConsumer(); 129 | Iterator it = list.iterator(); 130 | list.forEach(myConsumer); 131 | } 132 | static class MyConsumer implements Consumer { 133 | @Override 134 | public void accept(Object t) { 135 | System.out.println("自定义打印:" + t); 136 | } 137 | 138 | } 139 | 140 | } 141 | ``` 142 | 输出的结果: 143 | ```java 144 | 自定义打印:Jam 145 | 自定义打印:Jane 146 | 自定义打印:Sam 147 | ``` 148 | ### 1.3 spliterator()方法 149 | 这是一个为了并行遍历数据元素而设计的迭代方法,返回的是`Spliterator`,是专门并行遍历的迭代器。以发挥多核时代的处理器性能,java默认在集合框架中提供了一个默认的`Spliterator`实现,底层也就是Stream.isParallel()实现的,我们可以看一下源码: 150 | ```java 151 | // stream使用的就是spliterator 152 | default Stream stream() { 153 | return StreamSupport.stream(spliterator(), false); 154 | } 155 | default Spliterator spliterator() { 156 | return Spliterators.spliterator(this, 0); 157 | } 158 | public static Stream stream(Spliterator spliterator, boolean parallel) { 159 | Objects.requireNonNull(spliterator); 160 | return new ReferencePipeline.Head<>(spliterator, 161 | StreamOpFlag.fromCharacteristics(spliterator), 162 | parallel); 163 | } 164 | ``` 165 | 使用的方法如下: 166 | ```java 167 | public static void spliterator(){ 168 | List list = Arrays.asList("1", "2", "3","4","5","6","7","8","9","10"); 169 | // 获取可迭代器 170 | Spliterator spliterator = list.spliterator(); 171 | // 一个一个遍历 172 | System.out.println("tryAdvance: "); 173 | spliterator.tryAdvance(item->System.out.print(item+" ")); 174 | spliterator.tryAdvance(item->System.out.print(item+" ")); 175 | System.out.println("\n-------------------------------------------"); 176 | 177 | // 依次遍历剩下的 178 | System.out.println("forEachRemaining: "); 179 | spliterator.forEachRemaining(item->System.out.print(item+" ")); 180 | System.out.println("\n------------------------------------------"); 181 | 182 | // spliterator1:0~10 183 | Spliterator spliterator1 = list.spliterator(); 184 | // spliterator1:6~10 spliterator2:0~5 185 | Spliterator spliterator2 = spliterator1.trySplit(); 186 | // spliterator1:8~10 spliterator3:6~7 187 | Spliterator spliterator3 = spliterator1.trySplit(); 188 | System.out.println("spliterator1: "); 189 | spliterator1.forEachRemaining(item->System.out.print(item+" ")); 190 | System.out.println("\n------------------------------------------"); 191 | System.out.println("spliterator2: "); 192 | spliterator2.forEachRemaining(item->System.out.print(item+" ")); 193 | System.out.println("\n------------------------------------------"); 194 | System.out.println("spliterator3: "); 195 | spliterator3.forEachRemaining(item->System.out.print(item+" ")); 196 | } 197 | ``` 198 | - tryAdvance() 一个一个元素进行遍历 199 | - forEachRemaining() 顺序地分块遍历 200 | - trySplit()进行分区形成另外的 Spliterator,使用在并行操作中,分出来的是前面一半,就是不断把前面一部分分出来 201 | 202 | 结果如下: 203 | ```java 204 | tryAdvance: 205 | 1 2 206 | ------------------------------------------- 207 | forEachRemaining: 208 | 3 4 5 6 7 8 9 10 209 | ------------------------------------------ 210 | spliterator1: 211 | 8 9 10 212 | ------------------------------------------ 213 | spliterator2: 214 | 1 2 3 4 5 215 | ------------------------------------------ 216 | spliterator3: 217 | 6 7 218 | ``` 219 | 还有一些其他的用法在这里就不列举了,主要是trySplit()之后,可以用于多线程遍历。理想的时候,可以平均分成两半,有利于并行计算,但是不是一定平分的。 220 | 221 | # 总结 222 | 以上可以得知,`iterable`接口,主要是定义了迭代遍历的规范,这个接口的作用是获取迭代器,迭代器在JDK1.8版本增加了可分割迭代器,更有利于并发处理。`iterable`接口,从字面意义来说,就是可以迭代的意思,可以理解为实现这个接口的集合类获得了迭代遍历的能力,同时它也是集合的顶级接口,`Collection`接口继承了它。 223 | 224 | **【作者简介】**: 225 | 秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。 226 | 227 | 此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~ 228 | 229 | 230 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201012000828.png) -------------------------------------------------------------------------------- /java基础/java集合基础/4.iterator和iterable异同详解.md: -------------------------------------------------------------------------------- 1 | [toc] 2 | # 一、iterator介绍 3 | `iterator`接口,也是集合大家庭中的一员。和其他的`Map`和`Collection`接口不同,`iterator` 主要是为了方便遍历集合中的所有元素,用于迭代访问集合中的元素,相当于定义了遍历元素的规范,而另外的`Map`和`Collection`接口主要是定义了存储元素的规范。 4 | ```java 5 | boolean hasNext(); // 是否有下一个元素 6 | 7 | E next(); // 获取下一个元素 8 | 9 | // 移除元素 10 | default void remove() { 11 | throw new UnsupportedOperationException("remove"); 12 | } 13 | 14 | // 对剩下的所有元素进行处理,action则为处理的动作,意为要怎么处理 15 | default void forEachRemaining(Consumer action) { 16 | Objects.requireNonNull(action); 17 | while (hasNext()) 18 | action.accept(next()); 19 | } 20 | ``` 21 | 22 | **为什么需要iterator接口?** 23 | 24 | 首先,我们知道`iterator`接口是为了定义遍历集合的规范,也是一种抽象,把在不同集合的遍历方式抽象出来,这样遍历的时候,就不需要知道不同集合的内部结构。 25 | 26 | > 为什么需要抽象? 27 | 28 | 假设没有`iterator`接口,我们知道,遍历的时候只能通过索引,比如 29 | ```java 30 | for(int i=0;i iterator(); 43 | 44 | // 遍历内部元素,action意思为动作,指可以对每个元素进行操作(JDK1.8添加) 45 | default void forEach(Consumer action) {} 46 | 47 | // 创建并返回一个可分割迭代器(JDK1.8添加),分割的迭代器主要是提供可以并行遍历元素的迭代器,可以适应现在cpu多核的能力,加快速度。 48 | default Spliterator spliterator() { 49 | return Spliterators.spliteratorUnknownSize(iterator(), 0); 50 | } 51 | ``` 52 | 从上面的源码可以看出,`iterable`接口主要是为了获取`iterator`,附带了一个`foreach()`方法。 53 | 集合`Collection`、`List`、`Set`都是Iterable的实现类,它们及其他们的子类都可以使用foreach进行迭代。 54 | 55 | 56 | # 三、为什么有Iterator还需要Iterable 57 | 我们看到`Iterator`其实已经有很多处理集合元素相关的方法了,为什么还需要抽象一层呢?很多集合不直接实现`Iterator`接口,而是实现`Iterable`? 58 | 59 | 1.`Iterator`接口的核心方法next()或者hashNext(),previous()等,都是严重依赖于指针的,也就是迭代的目前的位置。如果Collection直接实现`Iterator`接口,那么集合对象就拥有了指针的能力,内部不同方法传递,就会让next()方法互相受到阻挠。只有一个迭代位置,互相干扰。 60 | 2.`Iterable` 每次获取迭代器,就会返回一个从头开始的,不会和其他的迭代器相互影响。 61 | 3.这样子也是解耦合的一种,有些集合不止有一个`Iterator`内部类,可能有两个,比如`ArrayList`,`LinkedList`,可以获取不同的`Iterator`执行不一样的操作。 62 | 63 | 64 | **【作者简介】**: 65 | 秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。 66 | 67 | 此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~ 68 | 69 | 70 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201012000828.png) -------------------------------------------------------------------------------- /java基础/java集合基础/6.1Collections和Collection的区别.md: -------------------------------------------------------------------------------- 1 | 刚开始学java的时候,分不清`Collection`和`Collections`,其实这两个东西是完全不一样的东西。 2 | 3 | >* Collection是一个接口,是java集合中的顶级接口之一,衍生出了java集合的庞大的体系。下面的图可以说明: 4 | 5 | 继承`Collection`的子类关系如下: 6 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20200229141352.png) 7 | 8 | 既然`Collection`是接口,那么它本身就是不可以实例化的,它的子类或者实现类是可以的。 9 | [【java集合梳理】— Collection接口详解](href="https://blog.csdn.net/Aphysia/article/details/104598709"%3Ehttps://blog.csdn.net/Aphysia/article/details/104598709%3C/a%3E) 10 | 11 | 12 | 而`Collections`则是工具类,是java集合中常用的方法的一个小小汇总,覆盖了排序,搜索,线程安全之类的一些算法,里面基本都是静态方法,可以直接用类名调用。具体源码解析看这个: 13 | 14 | [【java集合梳理】— Collections接口源码解析](href="https://blog.csdn.net/Aphysia/article/details/104733722"%3Ehttps://blog.csdn.net/Aphysia/article/details/104733722%3C/a%3E) 15 | 16 | 两个东西相同之处,大概是都是和集合相关的,而`Collections`感觉名字起得不太好,应该改成`CollectionUtils`,一目了然😂😂😂(开个玩笑) 17 | 18 | **【作者简介】**: 19 | 秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。 20 | 21 | 此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~ 22 | 23 | 24 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201012000828.png) -------------------------------------------------------------------------------- /java基础/函数内交换数值会不会变?.md: -------------------------------------------------------------------------------- 1 | 2 | ## 基础数据类型交换 3 | 这个话题,需要从最最基础的一道题目说起,看题目:以下代码a和b的值会交换么: 4 | ```java 5 | public static void main(String[] args) { 6 | int a = 1, b = 2; 7 | swapInt(a, b); 8 | System.out.println("a=" + a + " , b=" + b); 9 | } 10 | private static void swapInt(int a, int b) { 11 | int temp = a; 12 | a = b; 13 | b = temp; 14 | } 15 | ``` 16 | 结果估计大家都知道,a和b并没有交换: 17 | ```shell 18 | integerA=1 , integerB=2 19 | ``` 20 | 21 | 但是原因呢?先看这张图,先来说说Java虚拟机的结构: 22 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210124193442.png) 23 | 24 | 运行时区域主要分为: 25 | - 线程私有: 26 | - 程序计数器:`Program Count Register`,线程私有,没有垃圾回收 27 | - 虚拟机栈:`VM Stack`,线程私有,没有垃圾回收 28 | - 本地方法栈:`Native Method Stack`,线程私有,没有垃圾回收 29 | - 线程共享: 30 | - 方法区:`Method Area`,以`HotSpot`为例,`JDK1.8`后元空间取代方法区,有垃圾回收。 31 | - 堆:`Heap`,垃圾回收最重要的地方。 32 | 33 | 和这个代码相关的主要是虚拟机栈,也叫方法栈,是每一个线程私有的。 34 | 生命周期和线程一样,主要是记录该线程Java方法执行的内存模型。虚拟机栈里面放着好多**栈帧**。**注意虚拟机栈,对应是Java方法,不包括本地方法。** 35 | 36 | **一个Java方法执行会创建一个栈帧**,一个栈帧主要存储: 37 | - 局部变量表 38 | - 操作数栈 39 | - 动态链接 40 | - 方法出口 41 | 每一个方法调用的时候,就相当于将一个**栈帧**放到虚拟机栈中(入栈),方法执行完成的时候,就是对应着将该栈帧从虚拟机栈中弹出(出栈)。 42 | 43 | 每一个线程有一个自己的虚拟机栈,这样就不会混起来,如果不是线程独立的话,会造成调用混乱。 44 | 45 | 大家平时说的java内存分为堆和栈,其实就是为了简便的不太严谨的说法,他们说的栈一般是指虚拟机栈,或者虚拟机栈里面的局部变量表。 46 | 47 | 局部变量表一般存放着以下数据: 48 | - 基本数据类型(`boolean`,`byte`,`char`,`short`,`int`,`float`,`long`,`double`) 49 | - 对象引用(reference类型,不一定是对象本身,可能是一个对象起始地址的引用指针,或者一个代表对象的句柄,或者与对象相关的位置) 50 | - returAddress(指向了一条字节码指令的地址) 51 | 52 | 局部变量表内存大小编译期间确定,运行期间不会变化。空间衡量我们叫Slot(局部变量空间)。64位的long和double会占用2个Slot,其他的数据类型占用1个Slot。 53 | 54 | 上面的方法调用的时候,实际上栈帧是这样的,调用main()函数的时候,会往虚拟机栈里面放一个栈帧,栈帧里面我们主要关注局部变量表,传入的参数也会当成局部变量,所以第一个局部变量就是参数`args`,由于这个是`static`方法,也就是类方法,所以不会有当前对象的指针。 55 | 56 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203153821.png) 57 | 58 | 如果是普通方法,那么局部变量表里面会多出一个局部变量`this`。 59 | 60 | 如何证明这个东西真的存在呢?我们大概看看字节码,因为局部变量在编译的时候就确定了,运行期不会变化的。下面是`IDEA`插件`jclasslib`查看的: 61 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203154054.png) 62 | 63 | 上面的图,我们在`main()`方法的局部变量表中,确实看到了三个变量:`args`,`a`,`b`。 64 | 65 | **那在main()方法里面调用了swapInt(a, b)呢?** 66 | 67 | 那堆栈里面就会放入`swapInt(a,b)`的栈帧,**相当于把a和b局部变量复制了一份**,变成下面这样,由于里面一共有三个局部变量: 68 | - a:参数 69 | - b:参数 70 | - temp:函数内临时变量 71 | 72 | a和b交换之后,其实`swapInt(a,b)`的栈帧变了,a变为2,b变为1,但是`main()`栈帧的a和b并没有变。 73 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203160207.png) 74 | 75 | 那同样来从字节码看,会发现确实有3个局部变量在局部变量表内,并且他们的数值都是int类型。 76 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203154758.png) 77 | 78 | 而`swap(a,b)`执行结束之后,该方法的堆栈会被弹出虚拟机栈,此时虚拟机栈又剩下`main()`方法的栈帧,由于基础数据类型的数值相当于存在局部变量中,`swap(a,b)`栈帧中的局部变量不会影响`main()`方法的栈帧中的局部变量,所以,就算你在`swap(a,b)`中交换了,也不会变。 79 | 80 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203163654.png) 81 | 82 | ## 基础包装数据类型交换 83 | 将上面的数据类型换成包装类型,也就是`Integer`对象,结果会如何呢? 84 | ```java 85 | public static void main(String[] args) { 86 | Integer a = 1, b = 2; 87 | swapInteger(a, b); 88 | System.out.println("a=" + a + " , b=" + b); 89 | } 90 | private static void swapInteger(Integer a, Integer b) { 91 | Integer temp = a; 92 | a = b; 93 | b = temp; 94 | } 95 | ``` 96 | 97 | 结果还是一样,交换无效: 98 | ```shell 99 | a=1 , b=2 100 | ``` 101 | 102 | 这个怎么解释呢? 103 | 104 | 对象类型已经不是基础数据类型了,局部变量表里面的变量存的不是数值,而是对象的引用了。先用`jclasslib`查看一下字节码里面的局部变量表,发现其实和上面差不多,只是描述符变了,从`int`变成`Integer`。 105 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203161807.png) 106 | 107 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203161835.png) 108 | 109 | 但是和基础数据类型不同的是,局部变量里面存在的其实是堆里面真实的对象的引用地址,通过这个地址可以找到对象,比如,执行`main()`函数的时候,虚拟机栈如下: 110 | 111 | 假设 a 里面记录的是 1001 ,去堆里面找地址为 1001 的对象,对象里面存了数值1。b 里面记录的是 1002 ,去堆里面找地址为 1002 的对象,对象里面存了数值2。 112 | 113 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203163949.png) 114 | 115 | 而执行`swapInteger(a,b)`的时候,但是还没有交换的时候,相当于把 局部变量复制了一份: 116 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203164756.png) 117 | 118 | 而两者交换之后,其实是`SwapInteger(a,b)`栈帧中的a里面存的地址引用变了,指向了b,但是b里面的,指向了a。 119 | 120 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203165041.png) 121 | 122 | 而`swapInteger()`执行结束之后,其实`swapInteger(a,b)`的栈帧会退出虚拟机栈,只留下`main()`的栈帧。 123 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203163949.png) 124 | 125 | 这时候,a其实还是指向1,b还是指向2,因此,交换是没有起效果的。 126 | 127 | ## String,StringBuffer,自定义对象交换 128 | 一开始,我以为`String`不会变是因为`final`修饰的,但是实际上,不变是对的,但是不是这个原因。原因和上面的差不多。 129 | 130 | `String`是不可变的,只是说堆/常量池内的数据本身不可变。但是引用还是一样的,和上面分析的`Integer`一样。 131 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210203165910.png) 132 | 133 | 其实`StringBuffer`和自定义对象都一样,局部变量表内存在的都是引用,所以交换是不会变化的,因为`swap()`函数内的栈帧不会影响调用它的函数的栈帧。 134 | 135 | 不行我们来测试一下,用事实说话: 136 | ```java 137 | public static void main(String[] args) { 138 | String a = new String("1"), b = new String("2"); 139 | swapString(a, b); 140 | System.out.println("a=" + a + " , b=" + b); 141 | 142 | StringBuffer stringBuffer1 = new StringBuffer("1"), stringBuffer2 = new StringBuffer("2"); 143 | swapStringBuffer(stringBuffer1, stringBuffer2); 144 | System.out.println("stringBuffer1=" + stringBuffer1 + " , stringBuffer2=" + stringBuffer2); 145 | 146 | Person person1 = new Person("person1"); 147 | Person person2 = new Person("person2"); 148 | swapObject(person1,person2); 149 | System.out.println("person1=" + person1 + " , person2=" + person2); 150 | } 151 | 152 | private static void swapString(String s1,String s2){ 153 | String temp = s1; 154 | s1 = s2; 155 | s2 = temp; 156 | } 157 | 158 | private static void swapStringBuffer(StringBuffer s1,StringBuffer s2){ 159 | StringBuffer temp = s1; 160 | s1 = s2; 161 | s2 = temp; 162 | } 163 | 164 | private static void swapObject(Person p1,Person p2){ 165 | Person temp = p1; 166 | p1 = p2; 167 | p2 = temp; 168 | } 169 | 170 | 171 | class Person{ 172 | String name; 173 | 174 | public Person(String name){ 175 | this.name = name; 176 | } 177 | 178 | @Override 179 | public String toString() { 180 | return "Person{" + 181 | "name='" + name + '\'' + 182 | '}'; 183 | } 184 | } 185 | ``` 186 | 187 | 执行结果,证明交换确实没有起效果。 188 | ```java 189 | a=1 , b=2 190 | stringBuffer1=1 , stringBuffer2=2 191 | person1=Person{name='person1'} , person2=Person{name='person2'} 192 | ``` 193 | 194 | ## 总结 195 | 基础数据类型交换,栈帧里面存的是局部变量的数值,交换的时候,两个栈帧不会干扰,`swap(a,b)`执行完成退出栈帧后,`main()`的局部变量表还是以前的,所以不会变。 196 | 197 | 对象类型交换,栈帧里面存的是对象的地址引用,交换的时候,只是`swap(a,b)`的局部变量表的局部变量里面存的引用地址变化了,同样`swap(a,b)`执行完成退出栈帧后,`main()`的局部变量表还是以前的,所以不会变。 198 | 199 | 所以不管怎么交换都是不会变的。 200 | 201 | 202 | -------------------------------------------------------------------------------- /分布式/雪花算法的细枝末节讲解.md: -------------------------------------------------------------------------------- 1 | 前面文章在谈论分布式唯一ID生成的时候,有提到雪花算法,这一次,我们详细点讲解,只讲它。 2 | 3 | ## SnowFlake算法 4 | 5 | > 据国家大气研究中心的查尔斯·奈特称,一般的雪花大约由10^19个水分子组成。在雪花形成过程中,会形成不同的结构分支,所以说大自然中不存在两片完全一样的雪花,每一片雪花都拥有自己漂亮独特的形状。雪花算法表示生成的id如雪花般独一无二。 6 | > 7 | > snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。 8 | 9 | 10 | 11 | 核心思想:分布式,唯一。 12 | 13 | ## 算法具体介绍 14 | 15 | 雪花算法是 64 位 的二进制,一共包含了四部分: 16 | 17 | - 1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数。 18 | - 41位是时间戳,具体到毫秒,41位的二进制可以使用69年,因为时间理论上永恒递增,所以根据这个排序是可以的。 19 | - 10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID,10位最多可以表示1024台机器。 20 | - 12位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12位的序列号能够区分出4096个ID。 21 | 22 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211015001825.png) 23 | 24 | ### 优化 25 | 26 | 由于41位是时间戳,我们的时间计算是从1970年开始的,只能使用69年,为了不浪费,其实我们可以用时间的相对值,也就是以项目开始的时间为基准时间,往后可以使用69年。获取唯一ID的服务,对处理速度要求比较高,所以我们全部使用位运算以及位移操作,获取当前时间可以使用`System.currentTimeMillis()`。 27 | 28 | ### 时间回拨问题 29 | 30 | 在获取时间的时候,可能会出现`时间回拨`的问题,什么是时间回拨问题呢?就是服务器上的时间突然倒退到之前的时间。 31 | 32 | 1. 人为原因,把系统环境的时间改了。 33 | 2. 有时候不同的机器上需要同步时间,可能不同机器之间存在误差,那么可能会出现时间回拨问题。 34 | 35 | 36 | 37 | **解决方案** 38 | 39 | 1. 回拨时间小的时候,不生成 ID,循环等待到时间点到达。 40 | 2. 上面的方案只适合时钟回拨较小的,如果间隔过大,阻塞等待,肯定是不可取的,因此要么超过一定大小的回拨直接报错,拒绝服务,或者有一种方案是利用拓展位,回拨之后在拓展位上加1就可以了,这样ID依然可以保持唯一。但是这个要求我们提前预留出位数,要么从机器id中,要么从序列号中,腾出一定的位,在时间回拨的时候,这个位置 `+1`。 41 | 42 | 由于时间回拨导致的生产重复的ID的问题,其实百度和美团都有自己的解决方案了,有兴趣可以去看看,下面不是它们官网文档的信息: 43 | 44 | - 百度UIDGenerator:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md 45 | - UidGenerator是Java实现的, 基于[Snowflake](https://github.com/twitter/snowflake)算法的唯一ID生成器。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略, 从而适用于[docker](https://www.docker.com/)等虚拟化环境下实例自动重启、漂移等场景。 在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题. 最终单机QPS可达600万。 46 | - 美团Leaf:https://tech.meituan.com/2019/03/07/open-source-project-leaf.html 47 | - leaf-segment 方案 48 | - 优化:双buffer + 预分配 49 | - 容灾:Mysql DB 一主两从,异地机房,半同步方式 50 | - 缺点:如果用segment号段式方案:id是递增,可计算的,不适用于订单ID生成场景,比如竞对在两天中午12点分别下单,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。 51 | - leaf-snowflake方案 52 | - 使用Zookeeper持久顺序节点的特性自动对snowflake节点配置workerID 53 | - 1.启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。 54 | - 2.如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。 55 | - 3.如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。 56 | - 缓存workerID,减少第三方组件的依赖 57 | - 由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。**或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警** 58 | 59 | ## 代码展示 60 | 61 | ```java 62 | public class SnowFlake { 63 | 64 | // 数据中心(机房) id 65 | private long datacenterId; 66 | // 机器ID 67 | private long workerId; 68 | // 同一时间的序列 69 | private long sequence; 70 | 71 | public SnowFlake(long workerId, long datacenterId) { 72 | this(workerId, datacenterId, 0); 73 | } 74 | 75 | public SnowFlake(long workerId, long datacenterId, long sequence) { 76 | // 合法判断 77 | if (workerId > maxWorkerId || workerId < 0) { 78 | throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); 79 | } 80 | if (datacenterId > maxDatacenterId || datacenterId < 0) { 81 | throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); 82 | } 83 | System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", 84 | timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId); 85 | 86 | this.workerId = workerId; 87 | this.datacenterId = datacenterId; 88 | this.sequence = sequence; 89 | } 90 | 91 | // 开始时间戳(2021-10-16 22:03:32) 92 | private long twepoch = 1634393012000L; 93 | 94 | // 机房号,的ID所占的位数 5个bit 最大:11111(2进制)--> 31(10进制) 95 | private long datacenterIdBits = 5L; 96 | 97 | // 机器ID所占的位数 5个bit 最大:11111(2进制)--> 31(10进制) 98 | private long workerIdBits = 5L; 99 | 100 | // 5 bit最多只能有31个数字,就是说机器id最多只能是32以内 101 | private long maxWorkerId = -1L ^ (-1L << workerIdBits); 102 | 103 | // 5 bit最多只能有31个数字,机房id最多只能是32以内 104 | private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); 105 | 106 | // 同一时间的序列所占的位数 12个bit 111111111111 = 4095 最多就是同一毫秒生成4096个 107 | private long sequenceBits = 12L; 108 | 109 | // workerId的偏移量 110 | private long workerIdShift = sequenceBits; 111 | 112 | // datacenterId的偏移量 113 | private long datacenterIdShift = sequenceBits + workerIdBits; 114 | 115 | // timestampLeft的偏移量 116 | private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; 117 | 118 | // 序列号掩码 4095 (0b111111111111=0xfff=4095) 119 | // 用于序号的与运算,保证序号最大值在0-4095之间 120 | private long sequenceMask = -1L ^ (-1L << sequenceBits); 121 | 122 | // 最近一次时间戳 123 | private long lastTimestamp = -1L; 124 | 125 | 126 | // 获取机器ID 127 | public long getWorkerId() { 128 | return workerId; 129 | } 130 | 131 | 132 | // 获取机房ID 133 | public long getDatacenterId() { 134 | return datacenterId; 135 | } 136 | 137 | 138 | // 获取最新一次获取的时间戳 139 | public long getLastTimestamp() { 140 | return lastTimestamp; 141 | } 142 | 143 | 144 | // 获取下一个随机的ID 145 | public synchronized long nextId() { 146 | // 获取当前时间戳,单位毫秒 147 | long timestamp = timeGen(); 148 | 149 | if (timestamp < lastTimestamp) { 150 | System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp); 151 | throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", 152 | lastTimestamp - timestamp)); 153 | } 154 | 155 | // 去重 156 | if (lastTimestamp == timestamp) { 157 | 158 | sequence = (sequence + 1) & sequenceMask; 159 | 160 | // sequence序列大于4095 161 | if (sequence == 0) { 162 | // 调用到下一个时间戳的方法 163 | timestamp = tilNextMillis(lastTimestamp); 164 | } 165 | } else { 166 | // 如果是当前时间的第一次获取,那么就置为0 167 | sequence = 0; 168 | } 169 | 170 | // 记录上一次的时间戳 171 | lastTimestamp = timestamp; 172 | 173 | // 偏移计算 174 | return ((timestamp - twepoch) << timestampLeftShift) | 175 | (datacenterId << datacenterIdShift) | 176 | (workerId << workerIdShift) | 177 | sequence; 178 | } 179 | 180 | private long tilNextMillis(long lastTimestamp) { 181 | // 获取最新时间戳 182 | long timestamp = timeGen(); 183 | // 如果发现最新的时间戳小于或者等于序列号已经超4095的那个时间戳 184 | while (timestamp <= lastTimestamp) { 185 | // 不符合则继续 186 | timestamp = timeGen(); 187 | } 188 | return timestamp; 189 | } 190 | 191 | private long timeGen() { 192 | return System.currentTimeMillis(); 193 | } 194 | 195 | public static void main(String[] args) { 196 | SnowFlake worker = new SnowFlake(1, 1); 197 | long timer = System.currentTimeMillis(); 198 | for (int i = 0; i < 10000; i++) { 199 | worker.nextId(); 200 | } 201 | System.out.println(System.currentTimeMillis()); 202 | System.out.println(System.currentTimeMillis() - timer); 203 | } 204 | 205 | } 206 | 207 | 208 | ``` 209 | 210 | ## 问题分析 211 | 212 | ### 1. 第一位为什么不使用? 213 | 214 | 在计算机的表示中,第一位是符号位,0表示整数,第一位如果是1则表示负数,我们用的ID默认就是正数,所以默认就是0,那么这一位默认就没有意义。 215 | 216 | 217 | 218 | ### 2.机器位怎么用? 219 | 220 | 机器位或者机房位,一共10 bit,如果全部表示机器,那么可以表示1024台机器,如果拆分,5 bit 表示机房,5bit表示机房里面的机器,那么可以有32个机房,每个机房可以用32台机器。 221 | 222 | 223 | 224 | ### 3. twepoch表示什么? 225 | 226 | 由于时间戳只能用69年,我们的计时又是从1970年开始的,所以这个`twepoch`表示从项目开始的时间,用生成ID的时间减去`twepoch`作为时间戳,可以使用更久。 227 | 228 | 229 | 230 | ### 4. -1L ^ (-1L << x) 表示什么? 231 | 232 | 表示 x 位二进制可以表示多少个数值,假设x为3: 233 | 234 | 在计算机中,第一位是符号位,负数的反码是除了符号位,1变0,0变1, 而补码则是反码+1: 235 | 236 | ```txt 237 | -1L 原码:1000 0001 238 | -1L 反码:1111 1110 239 | -1L 补码:1111 1111 240 | ``` 241 | 242 | 从上面的结果可以知道,**-1L其实在二进制里面其实就是全部为1**,那么 -1L 左移动 3位,其实得到 `1111 1000`,也就是最后3位是0,再与`-1L`异或计算之后,其实得到的,就是后面3位全是1。`-1L ^ (-1L << x) `表示的其实就是x位全是1的值,也就是x位的二进制能表示的最大数值。 243 | 244 | 245 | 246 | ### 5.时间戳比较 247 | 248 | 在获取时间戳小于上一次获取的时间戳的时候,不能生成ID,而是继续循环,直到生成可用的ID,这里没有使用拓展位防止时钟回拨。 249 | 250 | 251 | 252 | ### 6.前端直接使用发生精度丢失 253 | 254 | 如果前端直接使用服务端生成的long 类型 id,会发生精度丢失的问题,因为 JS 中Number是16位的(指的是十进制的数字),而雪花算法计算出来最长的数字是19位的,这个时候需要用 String 作为中间转换,输出到前端即可。 255 | 256 | ## 秦怀の观点 257 | 258 | 雪花算法其实是依赖于时间的一致性的,如果时间回拨,就可能有问题,一般使用拓展位解决。而只能使用69年这个时间限制,其实可以根据自己的需要,把时间戳的位数设置得更多一点,比如42位可以用139年,但是很多公司首先得活下来。当然雪花算法也不是银弹,它也有缺点,在单机上递增,而多台机器只是大致递增趋势,并不是严格递增的。 259 | 260 | 261 | 262 | **没有最好的设计方案,只有合适和不合适的方案。** 263 | -------------------------------------------------------------------------------- /工具/使用PicGo存储markdown图片(阿里云或者github).md: -------------------------------------------------------------------------------- 1 | #### PicGo代替极简图床 2 | 之前使用极简床图,但是后来好像挂了,真是一件悲伤的事,最近才发现了一个神器,开源的`PicGo`,已经有各个平台的版本了。链接如下:https://github.com/Molunerfinn/PicGo/releases 去下载自己的平台即可。虽然你要是`Mac`,有`iPic`也是很ok的。 3 | 4 | 下载好之后怎么配置呢? 5 | 6 | #### 配置阿里云 7 | 之前是注册阿里云认证之后就送一个`oss`存储(免费的),不知道现在还有没有,如果有的话就可以按照这个步骤配置下去。https://homenew.console.aliyun.com/ 8 | 9 | ![](https://img-blog.csdnimg.cn/img_convert/27958f9e224d9edc6f5029668a201799.png) 10 | 11 | 打开`oss`之后,需要自己建立`Bucket`(存储空间名),存储区域(比如`oss-cn-qingdao`),除此之外还需要`keyid`和`keysecret`。(一定要打开公共读的权限,要不是访问不了的,但是不要打开公共写!!!) 12 | 13 | ![](https://img-blog.csdnimg.cn/img_convert/75e3fc873828c7069d92bd164284ded2.png) 14 | 15 | 点击自己的头像,选择`accesskeys`,新建一个`key`就可以了,我是新建了公共的。 16 | 17 | ![](https://img-blog.csdnimg.cn/img_convert/66f855c24c8472b4a51d7b872ad0bf5e.png) 18 | 19 | 安装`PicGo`之后,打开,选择阿里云,把上面的东西配置进去就可以了。 20 | 21 | ![](https://img-blog.csdnimg.cn/img_convert/785414926c1b0662e8607aa195f6905a.png) 22 | 23 | 然后选择上传区,就可以上传了,选择不同的`markdown`之类的,也可以选择剪贴板上传,上传成功之后就会有提示,连接就在粘贴板了,直接去粘贴就可以了。 24 | 25 | #### 配置github 26 | 新建一个仓库,然后记得仓库的名字,点击头像,选择`settings`,`developer` `settings`--》`personal access token`--》新建一个`token`,记得勾选上权限,我全部选了,那个`token`只能查看一次,最好保存一下!!! 27 | 28 | 然后打开`PicGo`,选择`Github`图床,设置好就可以了。 29 | 30 | ![](https://img-blog.csdnimg.cn/img_convert/7668a762ad1413ab0f9700b7f0978bc9.png) 31 | 32 | **此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~** 33 | 34 | **技术之路不在一时,山高水长,纵使缓慢,驰而不息。** 35 | 36 | **公众号:秦怀杂货店** 37 | 38 | ![](https://img-blog.csdnimg.cn/img_convert/7d98fb66172951a2f1266498e004e830.png) -------------------------------------------------------------------------------- /工具/如何使用docsify搭建github文档.md: -------------------------------------------------------------------------------- 1 | ## 安装前提 2 | 确认电脑已经安装好 `node` 和 `npm` 环境。 如果还没有装好,那需要执行下面的步骤: 3 | 1.进入官网:https://nodejs.org/zh-cn/ , 下载长期支持版。 4 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210106234542.png) 5 | 6 | 2.安装就直接下一步就可以了,默认会把环境变量添加进去。 7 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210106234734.png) 8 | 9 | 3.直到finish,打开cmd命令行,查看环境变量以及版本。(**此时你们看到的应该还是只把node.js的根目录添加到环境变量path**) 10 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210106235050.png) 11 | 12 | 4.运行命令修改npm的文件夹前缀和缓存目录,配置镜像站。 13 | ```shell 14 | npm config set prefix "D:\nodejs\node_global" 15 | npm config set cache "D:\nodejs\node_cache" 16 | npm config set registry=http://registry.npm.taobao.org 17 | ``` 18 | 19 | 然后使用`npm config list`就可以看到自己的配置: 20 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210106235503.png) 21 | 22 | 还需要增加一个环境变量,是node的modules的环境变量(我的nodejs在D盘根目录下,你们的要自己根据实际情况): 23 | ```shell 24 | D:\nodejs\node_global\node_modules 25 | ``` 26 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210106235744.png) 27 | 28 | 5.然后如果使用`npm`安装了东西,但是找不到该命令,则还需要在Path中,把我们node的全局文件夹添加进去环境变量中。 29 | 30 | ```shell 31 | D:\nodejs\node_global 32 | ``` 33 | 34 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210107000041.png) 35 | 36 | 这样我们就可以愉快的安装东西了。 37 | 38 | 39 | ## docsify走起 40 | 官网: 41 | https://docsify.js.org/#/ 42 | 43 | 废话我就不多说了,直接安装`docsify-cli` : 44 | ```shell 45 | npm i docsify-cli -g 46 | ``` 47 | 48 | 然后我们建立一个测试文件夹叫`note`,命令行进入这个文件夹: 49 | ```shell 50 | cd note 51 | docsify init ./docs 52 | ``` 53 | 就成功了!!!看到它叫你执行命令,本地启动一下: 54 | ```shell 55 | docsify serve ./docs 56 | ``` 57 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210107000543.png) 58 | 59 | 这样就可以在本地http://localhost:3000打开了,神奇~(修改内容后保存就可以,不需要重新启动) 60 | 61 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210107000719.png) 62 | 63 | ## 美化一下 64 | 说实在话,挺丑的,那就美化一下: 65 | 先加一个封面,需要在`index.html中,把下面的属性设置为true 66 | ```shell 67 | coverpage: true 68 | ``` 69 | 然后新建一个文件`_coverpage.md`: 70 | ```txt 71 | # Mybatis摸索之路 72 | 73 | 74 | > 这是我自己的笔记啊啊啊啊 75 | 76 | [CSDN](https://blog.csdn.net/Aphysia) 77 | [滚动鼠标](#introduction) 78 | ``` 79 | 然后它就变成这样了: 80 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210107001308.png) 81 | 82 | 我们还需要一个侧边栏,再将侧边栏属性打开: 83 | ```shell 84 | loadSidebar: true 85 | ``` 86 | 然后新建一个侧边栏的文件`_sidebar.md`: 87 | ```txt 88 | - Note 89 | 90 | - [第一章节](第一章节.md) 91 | - [第二章节](第二章节.md) 92 | - [第三章节](第三章节.md) 93 | ``` 94 | 然后就变成这样了: 95 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210107001644.png) 96 | 97 | 其中中间那部分使用的是`README.md`的内容,其他的index.html的内容如下(自己根据需要设置,如果有更高级的需求,建议去官网查文档!!!) 98 | ```html 99 | 100 | 101 | 102 | 103 | 104 | docsify-demo 105 | 106 | 107 | 109 | 110 | 111 | 112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 147 | 148 | 149 | 150 | 151 | ``` 152 | 153 | ## 如何部署到github 154 | 下面讲讲如何部署,首先我们需要有一个远程的仓库,我默认你有了,使用命令初始化文件夹,关联远程仓库 155 | ```shell 156 | git init 157 | git remote add origin "自己在三方代码托管平台上所创建仓库对应的地址" 158 | ``` 159 | 160 | `push`代码到远程仓库就可以了,`git`的操作就不仔细讲了,或者自己把远程的仓库先`clone`下来,再用`docsify`创建文档,然后提交,也是ok的。 161 | 162 | 提交上去之后,我们需要做一个操作,在`settings`下有一个`GitHub Pages`,选择构建分支和文件目录即可。我使用的是`master`,根目录的`docs`文件夹。然后你就可以看到已经发布成功了,直接访问网址就可以。 163 | 164 | PS:项目是我的其他项目地址,但是流程是一致的。 165 | 166 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210107002821.png) 167 | 168 | 169 | ### 坑点 170 | 我打不开网址!!!是因为电信会屏蔽一些网站,也就是被qiang了,懂的都懂,如果要访问的话,可以修改DNS,或者搞一把梯_子。 171 | 172 | -------------------------------------------------------------------------------- /工具/平时画图的软件.md: -------------------------------------------------------------------------------- 1 | ## draw.io 2 | 之前画图一般都是使用processOn(`https://www.processon.com/`),缺点就是只能通过网站画图,并且存在云端,不花钱想白嫖,只能画几张。 3 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210130230928.png) 4 | 5 | 像我这种家庭条件,怎么用得起,而且我喜欢把东西存在自己的机器上,最好能有客户端,还能自己传到自己的存储上。 6 | 7 | 于是,我在`github`上找了`draw.io`客户端,其实也是有网页版的: 8 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210130231524.png) 9 | 10 | 客户端`github`地址: https://github.com/jgraph/drawio-desktop 11 | 12 | 下载地址: https://github.com/jgraph/drawio-desktop/releases 13 | 14 | > drawio-desktop是一个基于Electron的diagrams.net桌面画图应用程序。io是diagrams.net的旧名称,我们只是不想更改所有二进制文件的名称。 15 | 16 | 感受一下画图: 17 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210130232121.png) 18 | 19 | 基本什么流程图,结构图,都可以绘制,真的是良心软件。我的做法一般是存在本地,如果需要共享的就传到github公开仓库,要不就传到自己的私有仓库就可以。截图的时候可以把网格线这些去掉,干净一点点。 20 | 21 | 具体的操作细节就不演示了,推荐!!! 22 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210130232409.png) 23 | 24 | 25 | ## excalidraw 26 | 如果你觉得这个是不是太正式了?我想让图片更像手绘的,安排!!!`excalidraw`就是这么一个软件,也是开源的: https://github.com/excalidraw/excalidraw 27 | 28 | 中文字体地址:`https://board.oktangle.com/` 29 | 30 | 地址:`https://excalidraw.com/` 31 | 32 | 如果使用`docker`安装怎么办?地址:https://hub.docker.com/r/excalidraw/excalidraw 33 | 命令: 34 | 35 | ```java 36 | docker pull excalidraw/excalidraw 37 | ``` 38 | 39 | 但是我还是想用客户端,但是我没有`docker`怎么办? 40 | 41 | 一种方式是自己把仓库`clone`下来,自己`npm`编译,另外一种方式是可以在chrome的浏览器拓展里面下载,貌似我是打开`https://excalidraw.com/`的时候,会提示我下载插件,下载完成把插件快捷方式放在桌面即可。 42 | 43 | 来体会一下手绘的快乐: 44 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210130234851.png) 45 | 46 | 上面两个软件就是平日里我画图最多的软件了,各种结构图,流程图,说明图,应该走可以覆盖了。 47 | 48 | PS:手绘图,看着比较清新,但是貌似画多了会觉得不够简洁,大概是大道至简,还是`draw.io`好用,图形也很多。 49 | 50 | 51 | -------------------------------------------------------------------------------- /并发与多线程/线程池/如何设计一个线程池.md: -------------------------------------------------------------------------------- 1 | > 前一段时间在写一门算法课,总算是上线了,以及面试,所以没什么时间写,接下来的时间,应该会讲讲面试准备,刷题的一些东西,面了很多,通过面试的有平安,涂鸦智能,阿里,腾讯微保,虾皮,华为荣耀,微众,当然也有其他不少挂的,挂也正常,面试是一个双向选择,当然,我也还是个菜鸟。 2 | > 3 | > 以前,我总觉得的买一件东西,做一件事,或者从某一个时间节点开始,我的生命就会发生转折,一切就会无比顺利,立马变厉害。但是,事实上并不是如此。我不可能马上变厉害,也不可能一口吃成一个胖子。看一篇文章也不能让你从此走上人生巅峰,越来越相信,这是一个长期的过程,只有量变引起质变,纵使缓慢,驰而不息。 4 | 5 | # 如何设计一个线程池? 6 | 7 | ## 三个步骤 8 | 这是一个常见的问题,如果在比较熟悉线程池运作原理的情况下,这个问题并不难。设计实现一个东西,三步走:是什么?为什么?怎么做? 9 | 10 | ### 线程池是什么? 11 | 线程池使用了池化技术,将线程存储起来放在一个 "池子"(容器)里面,来了任务可以用已有的空闲的线程进行处理, 处理完成之后,归还到容器,可以复用。如果线程不够,还可以根据规则动态增加,线程多余的时候,亦可以让多余的线程死亡。 12 | 13 | ### 为什么要用线程池? 14 | 实现线程池有什么好处呢? 15 | 16 | - 降低资源消耗:池化技术可以重复利用已经创建的线程,降低线程创建和销毁的损耗。 17 | - 提高响应速度:利用已经存在的线程进行处理,少去了创建线程的时间 18 | - 管理线程可控:线程是稀缺资源,不能无限创建,线程池可以做到统一分配和监控 19 | - 拓展其他功能:比如定时线程池,可以定时执行任务 20 | 21 | ### 需要考虑的点 22 | 那线程池设计需要考虑的点: 23 | 24 | - 线程池状态: 25 | - 有哪些状态?如何维护状态? 26 | 27 | - 线程 28 | - 线程怎么封装?线程放在哪个池子里? 29 | - 线程怎么取得任务? 30 | - 线程有哪些状态? 31 | - 线程的数量怎么限制?动态变化?自动伸缩? 32 | - 线程怎么消亡?如何重复利用? 33 | - 任务 34 | - 任务少可以直接处理,多的时候,放在哪里? 35 | - 任务队列满了,怎么办? 36 | - 用什么队列? 37 | 38 | 39 | 40 | 如果从任务的阶段来看,分为以下几个阶段: 41 | - 如何存任务? 42 | - 如何取任务? 43 | - 如何执行任务? 44 | - 如何拒绝任务? 45 | 46 | 47 | 48 | ## 线程池状态 49 | 50 | ### 状态有哪些?如何维护状态? 51 | 52 | 状态可以设置为以下几种: 53 | 54 | - RUNNING:运行状态,可以接受任务,也可以处理任务 55 | - SHUTDOWN:不可以接受任务,但是可以处理任务 56 | - STOP:不可以接受任务,也不可以处理任务,中断当前任务 57 | - TIDYING:所有线程停止 58 | - TERMINATED:线程池的最后状态 59 | 60 | 各种状态之间是不一样的,他们的状态之间变化如下: 61 | 62 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210619211431.png) 63 | 64 | 而维护状态的话,可以用一个变量单独存储,并且需要保证修改时的**原子性**,在底层操作系统中,对int的修改是原子的,而在32位的操作系统里面,对`double`,`long`这种64位数值的操作不是原子的。**除此之外,实际上JDK里面实现的状态和线程池的线程数是同一个变量,高3位表示线程池的状态,而低29位则表示线程的数量。** 65 | 66 | 这样设计的好处是节省空间,并且同时更新的时候有优势。 67 | 68 | ## 线程相关 69 | 70 | ### 线程怎么封装?线程放在哪个池子里? 71 | 线程,即是实现了`Runnable`接口,执行的时候,调用的是`start()`方法,但是`start()`方法内部编译后调用的是 `run()` 方法,这个方法只能调用一次,调用多次会报错。因此线程池里面的线程跑起来之后,不可能终止再启动,只能一直运行着。**既然不可以停止,那么执行完任务之后,没有任务过来,只能是轮询取出任务的过程** 72 | 73 | 线程可以运行任务,因此封装线程的时候,假设封装成为 `Worker`, `Worker`里面必定是包含一个 `Thread`,表示当前线程,除了当前线程之外,封装的线程类还应该持有任务,初始化可能直接给予任务,当前的任务是null的时候才需要去获取任务。 74 | 75 | 可以考虑使用 `HashSet` 来存储线程,也就是充当线程池的角色,当然,`HashSet` 会有线程安全的问题需要考虑,那么我们可以考虑使用一个可重入锁比如 `ReentrantLock`,凡是增删线程池的线程,都需要锁住。 76 | 77 | ```java 78 | private final ReentrantLock mainLock = new ReentrantLock(); 79 | ``` 80 | 81 | ### 线程怎么取得任务? 82 | 83 | (1)初始化线程的时候可以直接指定任务,譬如`Runnable firstTask`,将任务封装到 `worker` 中,然后获取 `worker` 里面的 `thread`,`thread.run()`的时候,其实就是 跑的是 `worker` 本身的 `run()` 方法,因为 `worker` 本身就是实现了 `Runnable` 接口,里面的线程其实就是其本身。因此也可以实现对 `ThreadFactory` 线程工厂的定制化。 84 | 85 | ```java 86 | private final class Worker 87 | extends AbstractQueuedSynchronizer 88 | implements Runnable 89 | { 90 | final Thread thread; 91 | Runnable firstTask; 92 | 93 | ... 94 | 95 | Worker(Runnable firstTask) { 96 | setState(-1); // inhibit interrupts until runWorker 97 | this.firstTask = firstTask; 98 | // 从线程池创建线程,传入的是其本身 99 | this.thread = getThreadFactory().newThread(this); 100 | } 101 | } 102 | ``` 103 | 104 | (2)运行完任务的线程,应该继续取任务,取任务肯定需要从任务队列里面取,要是任务队列里面没有任务,由于是阻塞队列,那么可以等待,如果等待若干时间后,仍没有任务,倘若该线程池的线程数已经超过核心线程数,并且允许线程消亡的话,应该将该线程从线程池中移除,并结束掉该线程。 105 | 106 | > 取任务和执行任务,对于线程池里面的线程而言,就是一个周而复始的工作,除非它会消亡。 107 | 108 | ### 线程有哪些状态? 109 | 110 | 现在我们所说的是`Java`中的线程`Thread`,一个线程在一个给定的时间点,只能处于一种状态,这些状态都是虚拟机的状态,不能反映任何操作系统的线程状态,一共有六种/七种状态: 111 | 112 | - `NEW`:创建了线程对象,但是还没有调用`Start()`方法,还没有启动的线程处于这种状态。 113 | - `Running`:运行状态,其实包含了两种状态,但是`Java`线程将就绪和运行中统称为可运行 114 | - `Runnable`:就绪状态:创建对象后,调用了`start()`方法,该状态的线程还位于可运行线程池中,等待调度,获取`CPU`的使用权 115 | - 只是有资格执行,不一定会执行 116 | - `start()`之后进入就绪状态,`sleep()`结束或者`join()`结束,线程获得对象锁等都会进入该状态。 117 | - `CPU`时间片结束或者主动调用`yield()`方法,也会进入该状态 118 | - `Running` :获取到`CPU`的使用权(获得CPU时间片),变成运行中 119 | 120 | - `BLOCKED` :阻塞,线程阻塞于锁,等待监视器锁,一般是`Synchronize`关键字修饰的方法或者代码块 121 | - `WAITING` :进入该状态,需要等待其他线程通知(`notify`)或者中断,一个线程无限期地等待另一个线程。 122 | - `TIMED_WAITING` :超时等待,在指定时间后自动唤醒,返回,不会一直等待 123 | - `TERMINATED` :线程执行完毕,已经退出。如果已终止再调用start(),将会抛出`java.lang.IllegalThreadStateException`异常。 124 | 125 | ![image-20210509224848865](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210509224848865.png) 126 | 127 | ### 线程的数量怎么限制?动态变化?自动伸缩? 128 | 129 | 线程池本身,就是为了限制和充分使用线程资的,因此有了两个概念:核心线程数,最大线程数。 130 | 131 | 要想让线程数根据任务数量动态变化,那么我们可以考虑以下设计(假设不断有任务): 132 | 133 | - 来一个任务创建一个线程处理,直到线程数达到核心线程数。 134 | - 达到核心线程数之后且没有空闲线程,来了任务直接放到任务队列。 135 | - 任务队列如果是无界的,会被撑爆。 136 | - 任务队列如果是有界的,任务队列满了之后,还有任务过来,会继续创建线程处理,此时线程数大于核心线程数,直到线程数等于最大线程数。 137 | - 达到最大线程数之后,还有任务不断过来,会触发拒绝策略,根据不同策略进行处理。 138 | - 如果任务不断处理完成,任务队列空了,线程空闲没任务,会在一定时间内,销毁,让线程数保持在核心线程数即可。 139 | 140 | 141 | 142 | 由上面可以看出,主要控制伸缩的参数是`核心线程数`,`最大线程数`,`任务队列`,`拒绝策略`。 143 | 144 | ### 线程怎么消亡?如何重复利用? 145 | 146 | 线程不能被重新调用多次`start()`,因此只能调用一次,也就是线程不可能停下来,再启动。那么就说明线程复用只是在不断的循环罢了。 147 | 148 | 消亡只是结束了它的`run()`方法,当线程池数量需要自动缩容的,就会让一部分空闲的线程结束。 149 | 150 | 而重复利用,其实是执行完任务之后,再去去任务队列取任务,取不到任务会等待,任务队列是一个阻塞队列,这是一个`不断循环`的过程。 151 | 152 | ## 任务相关 153 | 154 | ### 任务少可以直接处理,多的时候,放在哪里? 155 | 156 | 任务少的时候,来了直接创建,赋予线程初始化任务,就可开始执行,任务多的时候,把它放进队列里面,先进先出。 157 | 158 | ### 任务队列满了,怎么办? 159 | 160 | 任务队列满了,会继续增加线程,直到达到最大的线程数。 161 | 162 | ### 用什么队列? 163 | 164 | 一般的队列,只是一个有限长度的缓冲区,要是满了,就不能保存当前的任务,阻塞队列可以通过阻塞,保留出当前需要入队的任务,只是会阻塞等待。同样的,阻塞队列也可以保证任务队列没有任务的时候,阻塞当前获取任务的线程,让它进入`wait`状态,释放`cpu`的资源。因此在线程池的场景下,阻塞队列其实是比较有必要的。 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /数据库/JDBC/1.jdbc教程入门之增删改查.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | #### 1.jdbc是什么 3 | > * `JDBC`(`Java DataBase Connectivity`,`java`数据库连接)是一种用于执行`SQL`语句的`Java API`,可以为多种关系数据库提供统一访问,它由一组用`Java`语言编写的类和接口组成。`JDBC`提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。(百度百科) 4 | > 5 | > * `jdbc`经常用来连接数据库,创建`sql`或者`mysql`语句,使用相关的`api`去执行`sql`语句,从而操作数据库,达到查看或者修改数据库的目的。 6 | > * 学习`jdbc`要求对`java`编程有一定了解,并了解一种数据库系统以及`sql`语句。 7 | > * 环境要求: 8 | > 1.本地装好`jdk`,并且装好`mysql`数据库,我是直接装过`wamp`带有`mysql`数据库/`docker`中安装的`mysql`。 9 | > 2.使用IDEA开发 10 | 11 | #### 2.使用IDEA开发 12 | ##### 2.1 创建数据库,数据表 13 | 我的`mysql`是使用`docker`创建的,如果是`windows`环境可以使用`wamp`较为方便。 14 | 15 | 数据库名字是`test`,数据表的名字是`student`,里面有四个字段,一个是`id`,也就是主键(自动递增),还有名字,年龄,成绩。最后先使用`sql`语句插入六个测试记录。 16 | ```mysql 17 | CREATE DATABASE `test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 18 | 19 | USE test; 20 | 21 | CREATE TABLE `student` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(20) NOT NULL , 22 | `age` INT NOT NULL , `score` DOUBLE NOT NULL , PRIMARY KEY (`id`)) ENGINE = MyISAM; 23 | 24 | INSERT INTO `student` VALUES (1, '小红', 26, 83); 25 | INSERT INTO `student` VALUES (2, '小白', 23, 93); 26 | INSERT INTO `student` VALUES (3, '小明', 34, 45); 27 | INSERT INTO `student` VALUES (4, '张三', 12, 78); 28 | INSERT INTO `student` VALUES (5, '李四', 33, 96); 29 | INSERT INTO `student` VALUES (6, '魏红', 23, 46); 30 | ``` 31 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210126165253.png) 32 | 33 | ##### 2.2 使用IDEA创建项目 34 | 35 | 我使用maven工程方式,项目目录: 36 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20200925011318.png) 37 | 38 | Student.class 39 | ```java 40 | package model; 41 | 42 | /** 43 | * student类,字段包括id,name,age,score 44 | * 实现无参构造,带参构造,toString方法,以及get,set方法 45 | */ 46 | public class Student { 47 | private int id; 48 | private String name; 49 | private int age; 50 | private double score; 51 | 52 | public Student() { 53 | super(); 54 | // TODO Auto-generated constructor stub 55 | } 56 | public Student(String name, int age, double score) { 57 | super(); 58 | this.name = name; 59 | this.age = age; 60 | this.score = score; 61 | } 62 | public int getId() { 63 | return id; 64 | } 65 | public void setId(int id) { 66 | this.id = id; 67 | } 68 | public String getName() { 69 | return name; 70 | } 71 | public void setName(String name) { 72 | this.name = name; 73 | } 74 | public int getAge() { 75 | return age; 76 | } 77 | public void setAge(int age) { 78 | this.age = age; 79 | } 80 | public double getScore() { 81 | return score; 82 | } 83 | public void setScore(double score) { 84 | this.score = score; 85 | } 86 | @Override 87 | public String toString() { 88 | return "Student [id=" + id + ", name=" + name + ", age=" + age 89 | + ", score=" + score + "]"; 90 | } 91 | 92 | } 93 | ``` 94 | 95 | DBUtil.class 96 | ```java 97 | package db; 98 | 99 | import java.sql.Connection; 100 | import java.sql.DriverManager; 101 | import java.sql.SQLException; 102 | /** 103 | * 工具类,获取数据库的连接 104 | * @author 秦怀 105 | * 106 | */ 107 | public class DBUtil { 108 | private static String URL="jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&serverTimezone=UTC"; 109 | private static String USER="root"; 110 | private static String PASSWROD ="123456"; 111 | private static Connection connection=null; 112 | static{ 113 | try { 114 | Class.forName("com.mysql.jdbc.Driver"); 115 | // 获取数据库连接 116 | connection=DriverManager.getConnection(URL,USER,PASSWROD); 117 | System.out.println("连接成功"); 118 | } catch (ClassNotFoundException e) { 119 | // TODO Auto-generated catch block 120 | e.printStackTrace(); 121 | } catch (SQLException e) { 122 | // TODO Auto-generated catch block 123 | e.printStackTrace(); 124 | } 125 | } 126 | // 返回数据库连接 127 | public static Connection getConnection(){ 128 | return connection; 129 | } 130 | } 131 | ``` 132 | 133 | StudentDao.class 134 | ```java 135 | package dao; 136 | 137 | import model.Student; 138 | 139 | import java.util.ArrayList; 140 | import java.util.List; 141 | import java.sql.Connection; 142 | import java.sql.PreparedStatement; 143 | import java.sql.ResultSet; 144 | import java.sql.SQLException; 145 | import java.sql.Statement; 146 | 147 | import db.DBUtil; 148 | /** 149 | * 操作学生表的dao类 150 | * @author 秦怀 151 | * 下面均使用预编译的方法 152 | */ 153 | public class StudentDao { 154 | //将连接定义为单例 155 | private static Connection connection = DBUtil.getConnection(); 156 | // 添加新的学生 157 | public void addStudent(Student student){ 158 | String sql ="insert into student(name,age,score) "+ 159 | "values(?,?,?)"; 160 | boolean result = false; 161 | try { 162 | // 将sql传进去预编译 163 | PreparedStatement preparedstatement = connection.prepareStatement(sql); 164 | // 下面把参数传进去 165 | preparedstatement.setString(1, student.getName()); 166 | preparedstatement.setInt(2, student.getAge()); 167 | preparedstatement.setDouble(3, student.getScore()); 168 | preparedstatement.execute(); 169 | } catch (SQLException e) { 170 | // TODO Auto-generated catch block 171 | e.printStackTrace(); 172 | System.out.println("创建数据库连接失败"); 173 | } 174 | } 175 | // 更新学生信息 176 | public void updateStudent(Student student){ 177 | String sql = "update student set name = ? ,age =?,score = ? where id = ? "; 178 | boolean result = false; 179 | try { 180 | PreparedStatement preparedStatement = connection.prepareStatement(sql); 181 | preparedStatement.setString(1, student.getName()); 182 | preparedStatement.setInt(2, student.getAge()); 183 | preparedStatement.setDouble(3, student.getScore()); 184 | preparedStatement.setInt(4, student.getId()); 185 | preparedStatement.executeUpdate(); 186 | } catch (SQLException e) { 187 | // TODO Auto-generated catch block 188 | e.printStackTrace(); 189 | System.out.println("连接数据库失败"); 190 | } 191 | } 192 | // 根据id删除一个学生 193 | public void deleteStudent(int id){ 194 | String sql = "delete from student where id = ?"; 195 | boolean result = false; 196 | try { 197 | PreparedStatement preparedStatement = connection.prepareStatement(sql); 198 | preparedStatement.setInt(1, id); 199 | result=preparedStatement.execute(); 200 | } catch (SQLException e) { 201 | // TODO Auto-generated catch block 202 | e.printStackTrace(); 203 | } 204 | } 205 | // 根据id查询学生 206 | public Student selectStudent(int id){ 207 | String sql ="select * from student where id =?"; 208 | try { 209 | PreparedStatement preparedStatement = connection.prepareStatement(sql); 210 | preparedStatement.setInt(1, id); 211 | ResultSet resultSet = preparedStatement.executeQuery(); 212 | Student student = new Student(); 213 | // 一条也只能使用resultset来接收 214 | while(resultSet.next()){ 215 | student.setId(resultSet.getInt("id")); 216 | student.setName(resultSet.getString("name")); 217 | student.setAge(resultSet.getInt("age")); 218 | student.setScore(resultSet.getDouble("score")); 219 | } 220 | return student; 221 | } catch (SQLException e) { 222 | // TODO: handle exception 223 | } 224 | return null; 225 | } 226 | // 查询所有学生,返回List 227 | public List selectStudentList(){ 228 | Liststudents = new ArrayList(); 229 | String sql ="select * from student "; 230 | try { 231 | PreparedStatement preparedStatement = DBUtil.getConnection().prepareStatement(sql); 232 | ResultSet resultSet = preparedStatement.executeQuery(); 233 | // 不能把student在循环外面创建,要不list里面六个对象都是一样的,都是最后一个的值, 234 | // 因为list add进去的都是引用 235 | // Student student = new Student(); 236 | while(resultSet.next()){ 237 | Student student = new Student(); 238 | student.setId(resultSet.getInt(1)); 239 | student.setName(resultSet.getString(2)); 240 | student.setAge(resultSet.getInt(3)); 241 | student.setScore(resultSet.getDouble(4)); 242 | students.add(student); 243 | 244 | } 245 | } catch (SQLException e) { 246 | // TODO: handle exception 247 | } 248 | return students; 249 | } 250 | } 251 | 252 | ``` 253 | > StudentAction.class 254 | 255 | ```java 256 | package action; 257 | 258 | import java.util.List; 259 | 260 | import dao.StudentDao; 261 | 262 | import model.Student; 263 | 264 | public class StudentAction { 265 | 266 | /** 267 | * @param args 268 | */ 269 | public static void main(String[] args) { 270 | StudentDao studentDao = new StudentDao(); 271 | // TODO Auto-generated method stub 272 | System.out.println("========================查询所有学生========================"); 273 | List students =studentDao.selectStudentList(); 274 | for(int i=0;i 321 | 322 | 323 | mysql 324 | mysql-connector-java 325 | 8.0.21 326 | 327 | 328 | ``` 329 | 330 | -------------------------------------------------------------------------------- /数据库/JDBC/5.jdbc预编译与拼接sql对比.md: -------------------------------------------------------------------------------- 1 | > * 在jdbc中,有三种方式执行sql,分别是使用Statement(sql拼接),PreparedStatement(预编译),还有一种CallableStatement(存储过程),在这里我就不介绍CallableStatement了,我们来看看Statement与PreparedStatement的区别。 2 | ##### 1. 创建数据库,数据表 3 | 数据库名字是test,数据表的名字是student,里面有四个字段,一个是id,也就是主键(自动递增),还有名字,年龄,成绩。最后先使用sql语句插入六个测试记录。 4 | ```mysql 5 | CREATE DATABASE `test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 6 | CREATE TABLE `student` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(20) NOT NULL , 7 | `age` INT NOT NULL , `score` DOUBLE NOT NULL , PRIMARY KEY (`id`)) ENGINE = MyISAM; 8 | INSERT INTO `student` VALUES (1, '小红', 26, 83); 9 | INSERT INTO `student` VALUES (2, '小白', 23, 93); 10 | INSERT INTO `student` VALUES (3, '小明', 34, 45); 11 | INSERT INTO `student` VALUES (4, '张三', 12, 78); 12 | INSERT INTO `student` VALUES (5, '李四', 33, 96); 13 | INSERT INTO `student` VALUES (6, '魏红', 23, 46); 14 | ``` 15 | 建立对应的学生类: 16 | ```java 17 | /** 18 | * student类,字段包括id,name,age,score 19 | * 实现无参构造,带参构造,toString方法,以及get,set方法 20 | * @author 秦怀 21 | */ 22 | public class Student { 23 | private int id; 24 | private String name; 25 | private int age; 26 | private double score; 27 | 28 | public Student() { 29 | super(); 30 | // TODO Auto-generated constructor stub 31 | } 32 | public Student(String name, int age, double score) { 33 | super(); 34 | this.name = name; 35 | this.age = age; 36 | this.score = score; 37 | } 38 | public int getId() { 39 | return id; 40 | } 41 | public void setId(int id) { 42 | this.id = id; 43 | } 44 | public String getName() { 45 | return name; 46 | } 47 | public void setName(String name) { 48 | this.name = name; 49 | } 50 | public int getAge() { 51 | return age; 52 | } 53 | public void setAge(int age) { 54 | this.age = age; 55 | } 56 | public double getScore() { 57 | return score; 58 | } 59 | public void setScore(double score) { 60 | this.score = score; 61 | } 62 | @Override 63 | public String toString() { 64 | return "Student [id=" + id + ", name=" + name + ", age=" + age 65 | + ", score=" + score + "]"; 66 | } 67 | 68 | } 69 | 70 | ``` 71 | ##### 2.Statement 72 | 先来看代码,下面是获取数据库连接的工具类 **DBUtil.class**: 73 | ```java 74 | public class DBUtil { 75 | private static String URL="jdbc:mysql://127.0.0.1:3306/test"; 76 | private static String USER="root"; 77 | private static String PASSWROD ="123456"; 78 | private static Connection connection=null; 79 | static{ 80 | try { 81 | Class.forName("com.mysql.jdbc.Driver"); 82 | // 获取数据库连接 83 | connection=DriverManager.getConnection(URL,USER,PASSWROD); 84 | System.out.println("连接成功"); 85 | } catch (ClassNotFoundException e) { 86 | // TODO Auto-generated catch block 87 | e.printStackTrace(); 88 | } catch (SQLException e) { 89 | // TODO Auto-generated catch block 90 | e.printStackTrace(); 91 | } 92 | } 93 | // 返回数据库连接 94 | public static Connection getConnection(){ 95 | return connection; 96 | } 97 | } 98 | ``` 99 | 下面是根据id查询学生信息的代码片段,返回student对象就能输出了: 100 | ```java 101 | public Student selectStudentByStatement(int id){ 102 | // 拼接sql语句 103 | String sql ="select * from student where id = "+id; 104 | try { 105 | // 获取statement对象 106 | Statement statement = DBUtil.getConnection().createStatement(); 107 | // 执行sql语句,返回 ResultSet 108 | ResultSet resultSet = statement.executeQuery(sql); 109 | Student student = new Student(); 110 | // 一条也只能使用resultset来接收 111 | while(resultSet.next()){ 112 | student.setId(resultSet.getInt("id")); 113 | student.setName(resultSet.getString("name")); 114 | student.setAge(resultSet.getInt("age")); 115 | student.setScore(resultSet.getDouble("score")); 116 | } 117 | return student; 118 | } catch (SQLException e) { 119 | // TODO: handle exception 120 | } 121 | return null; 122 | } 123 | 124 | ``` 125 | 我们可以看到整个流程是先获取到数据库的连接`Class.forName("com.mysql.jdbc.Driver"); connection=DriverManager.getConnection(URL,USER,PASSWROD);`获取到连接之后通过连接获取statement对象,通过statement来执行sql语句,返回resultset这个结果集,`Statement statement = DBUtil.getConnection().createStatement();ResultSet resultSet = statement.executeQuery(sql);`,值得注意的是,上面的sql是已经拼接好,写固定了的sql,所以很容易被注入,比如这句: 126 | ```java 127 | sql = "select * from user where name= '" + name + "' and password= '" + password+"'"; 128 | ``` 129 | 130 | 如果有人 131 | > * name = "name' or '1'= `1" 132 | > * password = "password' or '1'='1",那么整个语句就会变成: 133 | ```java 134 | sql = "select * from user where name= 'name' or '1'='1' and password= 'password' or '1'='1'"; 135 | ``` 136 | 那么就会返回所有的信息,所以这是很危险的。 137 | 还有更加危险的,是在后面加上删除表格的操作,不过一般我们都不会把这些权限开放的。 138 | ```java 139 | // 如果password = " ';drop table user;select * from user where '1'= '1" 140 | // 后面一句不会执行,但是这已经可以删除表格了 141 | sql = "select * from user where name= 'name' or '1'='1' and password= '' ;drop table user;select * from user where '1'= '1'"; 142 | ``` 143 | 所以预编译显得尤为重要了。 144 | ##### 3.PreparedStatement预编译 145 | 我们先来看看预编译的代码: 146 | ```java 147 | // 根据id查询学生 148 | public Student selectStudent(int id){ 149 | String sql ="select * from student where id =?"; 150 | try { 151 | PreparedStatement preparedStatement = DBUtil.getConnection()..prepareStatement(sql); 152 | preparedStatement.setInt(1, id); 153 | ResultSet resultSet = preparedStatement.executeQuery(); 154 | Student student = new Student(); 155 | // 一条也只能使用resultset来接收 156 | while(resultSet.next()){ 157 | student.setId(resultSet.getInt("id")); 158 | student.setName(resultSet.getString("name")); 159 | student.setAge(resultSet.getInt("age")); 160 | student.setScore(resultSet.getDouble("score")); 161 | } 162 | return student; 163 | } catch (SQLException e) { 164 | // TODO: handle exception 165 | } 166 | return null; 167 | } 168 | ``` 169 | 预编译也是同样需要获取到数据库连接对象connection,但是sql语句拼接的时候使用了**占位符?**,将含有占位符的sql当参数传进去,获取到PreparedStatement预编译的对象,最后是通过set来绑定参数,然后再去使用execute执行预编译过的代码。这样就避免了sql注入的问题,同时,由于sql已经编译过缓存在数据库中,所以执行起来不用再编译,速度就会比较快。 170 | ##### 4.为什么预编译可以防止sql注入 171 | > * 在使用占位符,或者说参数的时候,数据库已经将sql指令编译过,那么查询的格式已经订好了,也就是我们说的我已经明白你要做什么了,你要是将不合法的参数传进去,会有合法性检查,用户只需要提供参数给我,参数不会当成指令部分来执行,也就是预编译已经把指令以及参数部分区分开,参数部分不允许传指令进来。 172 | > 这样的好处查询速度提高,因为有了预编译缓存,方便维护,可读性增强,不会有很多单引号双引号,容易出错,防止大部分的sql注入,因为参数和sql指令部分数据库系统已经区分开。百度文库里面提到:传递给PreparedStatement对象的参数可以被强制进行类型转换,使开发人员可以确保在插入或查询数据时与底层的数据库格式匹配。 173 | 要是理解不透彻可以这么来理解:
174 | ```sql 175 | select * from student where name= ? 176 | ``` 177 | 预编译的时候是先把这句话编译了,生成sql模板,相当于生成了一个我知道你要查名字了,你把名字传给我,你现在想耍点小聪明,把字符串`'Jame' or '1=1'`传进去,你以为他会变成下面这样么: 178 | ``` 179 | select * from student where name= 'Jame' or '1=1' 180 | ``` 181 | **放心吧,不可能的,这辈子都不可能的啦,数据库都知道你要干嘛了,我不是有sql模板了么,数据库的心里想的是我叫你传名字给我,行,这名字有点长,想害我,可以,我帮你找,那么数据库去名字这一字段帮你找一个叫`'Jame' or '1=1'`的人,他心里想这人真逗,没有这个人,没有!!!** 182 | 所以这也就是为什么预编译可以防止sql注入的解释了,它是经过了解释器解释过的,解释的过程我就不啰嗦了,只要是对参数做转义,转义之后让它在拼接时只能表示字符串,不能变成查询语句。 183 | -------------------------------------------------------------------------------- /架构设计/布隆过滤器的三种实践.md: -------------------------------------------------------------------------------- 1 | 前面我们已经讲过布隆过滤器的原理,都理解是这么运行的,那么一般我们使用布隆过滤器,是怎么去使用呢?如果自己去实现,又是怎么实现呢? 2 | 3 | [TOC] 4 | 5 | ## 布隆过滤器 6 | **再念一次定义:** 7 | 8 | 布隆过滤器(`Bloom Filter`)是由布隆(`Burton Howard Bloom`)在 1970 年提出的,它实际上是由一个很长的二进制向量和一系列随机`hash`映射函数组成(说白了,就是用二进制数组存储数据的特征)。 9 | 10 | 譬如下面例子:有三个`hash`函数,那么“陈六”就会被三个`hash`函数分别`hash`,并且对位数组的长度,进行取余,分别hash到三个位置。 11 | 12 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308233900.png) 13 | 14 | 如果对原理还有不理解的地方,可以查看我的上一篇文章。 15 | 16 | ## 手写布隆过滤器 17 | 18 | 那么我们手写布隆过滤器的时候,首先需要一个位数组,在`Java`里面有一个封装好的位数组,`BitSet`。 19 | 20 | 简单介绍一下`BitSet`,也就是位图,里面实现了使用紧凑的存储空间来表示大空间的位数据。使用的时候,我们可以直接指定大小,也就是相当于创建出指定大小的位数组。 21 | ```java 22 | BitSet bits = new BitSet(size); 23 | ``` 24 | 25 | 同时,`BitSet`提供了大量的`API`,基本的操作主要包括: 26 | - 清空位数组的数据 27 | - 翻转某一位的数据 28 | - 设置某一位的数据 29 | - 获取某一位的数据 30 | - 获取当前的`bitSet`的位数 31 | 32 | 下面就讲一下,写一个简单的布隆过滤器需要考虑的点: 33 | - 位数组的大小空间,需要指定,其他相同的时候,位数组的大小越大,`hash`冲突的可能性越小。 34 | - 多个`hash`函数,我们需要使用`hash`数组来存,`hash`函数需要如何设置呢?为了避免冲突,我们应该使用多个不同的质数来当种子。 35 | - 方法:主要实现两个方法,一个往布隆过滤器里面添加元素,另一个是判断布隆过滤器是否包含某个元素。 36 | 37 | 下面是具体的实现,只是简单的模拟,不可用于生产环境,`hash`函数较为简单,主要是使用`hash`值得高低位进行异或,然后乘以种子,再对位数组大小进行取余数: 38 | ```java 39 | import java.util.BitSet; 40 | 41 | public class MyBloomFilter { 42 | 43 | // 默认大小 44 | private static final int DEFAULT_SIZE = Integer.MAX_VALUE; 45 | 46 | // 最小的大小 47 | private static final int MIN_SIZE = 1000; 48 | 49 | // 大小为默认大小 50 | private int SIZE = DEFAULT_SIZE; 51 | 52 | // hash函数的种子因子 53 | private static final int[] HASH_SEEDS = new int[]{3, 5, 7, 11, 13, 17, 19, 23, 29, 31}; 54 | 55 | // 位数组,0/1,表示特征 56 | private BitSet bitSet = null; 57 | 58 | // hash函数 59 | private HashFunction[] hashFunctions = new HashFunction[HASH_SEEDS.length]; 60 | 61 | // 无参数初始化 62 | public MyBloomFilter() { 63 | // 按照默认大小 64 | init(); 65 | } 66 | 67 | // 带参数初始化 68 | public MyBloomFilter(int size) { 69 | // 大小初始化小于最小的大小 70 | if (size >= MIN_SIZE) { 71 | SIZE = size; 72 | } 73 | init(); 74 | } 75 | 76 | private void init() { 77 | // 初始化位大小 78 | bitSet = new BitSet(SIZE); 79 | // 初始化hash函数 80 | for (int i = 0; i < HASH_SEEDS.length; i++) { 81 | hashFunctions[i] = new HashFunction(SIZE, HASH_SEEDS[i]); 82 | } 83 | } 84 | 85 | // 添加元素,相当于把元素的特征添加到位数组 86 | public void add(Object value) { 87 | for (HashFunction f : hashFunctions) { 88 | // 将hash计算出来的位置为true 89 | bitSet.set(f.hash(value), true); 90 | } 91 | } 92 | 93 | // 判断元素的特征是否存在于位数组 94 | public boolean contains(Object value) { 95 | boolean result = true; 96 | for (HashFunction f : hashFunctions) { 97 | result = result && bitSet.get(f.hash(value)); 98 | // hash函数只要有一个计算出为false,则直接返回 99 | if (!result) { 100 | return result; 101 | } 102 | } 103 | return result; 104 | } 105 | 106 | // hash函数 107 | public static class HashFunction { 108 | // 位数组大小 109 | private int size; 110 | // hash种子 111 | private int seed; 112 | 113 | public HashFunction(int size, int seed) { 114 | this.size = size; 115 | this.seed = seed; 116 | } 117 | 118 | // hash函数 119 | public int hash(Object value) { 120 | if (value == null) { 121 | return 0; 122 | } else { 123 | // hash值 124 | int hash1 = value.hashCode(); 125 | // 高位的hash值 126 | int hash2 = hash1 >>> 16; 127 | // 合并hash值(相当于把高低位的特征结合) 128 | int combine = hash1 ^ hash1; 129 | // 相乘再取余 130 | return Math.abs(combine * seed) % size; 131 | } 132 | } 133 | 134 | } 135 | 136 | public static void main(String[] args) { 137 | Integer num1 = new Integer(12321); 138 | Integer num2 = new Integer(12345); 139 | MyBloomFilter myBloomFilter =new MyBloomFilter(); 140 | System.out.println(myBloomFilter.contains(num1)); 141 | System.out.println(myBloomFilter.contains(num2)); 142 | 143 | myBloomFilter.add(num1); 144 | myBloomFilter.add(num2); 145 | 146 | System.out.println(myBloomFilter.contains(num1)); 147 | System.out.println(myBloomFilter.contains(num2)); 148 | 149 | } 150 | } 151 | ``` 152 | 运行结果,符合预期: 153 | ```txt 154 | false 155 | false 156 | true 157 | true 158 | ``` 159 | 160 | 但是上面的这种做法是不支持预期的误判率的,只是可以指定位数组的大小。 161 | 162 | 当然我们也可以提供数据量,以及期待的大致的误判率来初始化,大致的初始化代码如下: 163 | ```java 164 | // 带参数初始化 165 | public BloomFilter(int num,double rate) { 166 | // 计算位数组的大小 167 | this.size = (int) (-num * Math.log(rate) / Math.pow(Math.log(2), 2)); 168 | // hsah 函数个数 169 | this.hashSize = (int) (this.size * Math.log(2) / num); 170 | // 初始化位数组 171 | this.bitSet = new BitSet(size); 172 | } 173 | ``` 174 | 175 | ## Redis实现 176 | 平时我们可以选择使用`Redis`的特性于布隆过滤器,为什么呢?因为`Redis`里面有类似于`BitSet`的指令,比如设置位数组的值: 177 | ``` 178 | setbit key offset value 179 | ``` 180 | 上面的`key`是键,`offset`是偏移量,`value`就是`1`或者`0`。比如下面的就是将key1 的第7位置为1。 181 | 182 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210314110134.png) 183 | 184 | 而获取某一位的数值可以使用下面这个命令: 185 | ```java 186 | gitbit key offset 187 | ``` 188 | 借助`redis`这个功能我们可以实现优秀的布隆过滤器,但是实际上我们不需要自己去写了,`Redisson`这个客户端已经有较好的实现。 189 | 下面就是用法: 190 | 使用`maven`构建项目,首先需要导包到`pom.xml`: 191 | ```xml 192 | 193 | 194 | org.redisson 195 | redisson 196 | 3.11.2 197 | 198 | 199 | ``` 200 | 代码如下,我使用的`docker`,启动的时候记得设置密码,运行的时候修改密码不起效果: 201 | ```shell 202 | docker run -d --name redis -p 6379:6379 redis --requirepass "password" 203 | ``` 204 | 205 | 实现的代码如下,首先需要连接上`redis`,然后创建`redission`,使用`redission`创建布隆过滤器,直接使用即可。(**可以指定预计的数量以及期待的误判率**) 206 | 207 | ```java 208 | import org.redisson.Redisson; 209 | import org.redisson.api.RBloomFilter; 210 | import org.redisson.api.RedissonClient; 211 | import org.redisson.config.Config; 212 | 213 | public class BloomFilterTest { 214 | public static void main(String[] args) { 215 | Config config = new Config(); 216 | config.useSingleServer().setAddress("redis://localhost:6379"); 217 | config.useSingleServer().setPassword("password"); 218 | // 相当于创建了redis的连接 219 | RedissonClient redisson = Redisson.create(config); 220 | 221 | RBloomFilter bloomFilter = redisson.getBloomFilter("myBloomFilter"); 222 | //初始化,预计元素数量为100000000,期待的误差率为4% 223 | bloomFilter.tryInit(100000000,0.04); 224 | //将号码10086插入到布隆过滤器中 225 | bloomFilter.add("12345"); 226 | 227 | System.out.println(bloomFilter.contains("123456"));//false 228 | System.out.println(bloomFilter.contains("12345"));//true 229 | } 230 | } 231 | ``` 232 | 运行结果如下:值得注意的是,这是单台`redis`的情况,如果是`redis`集群的做法略有不同。 233 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210314233301.png) 234 | 235 | ## Google GUAVA实现 236 | 237 | `Google`提供的`guava`包里面也提供了布隆过滤器,引入`pom`文件: 238 | ```xml 239 | 240 | com.google.guava 241 | guava 242 | 18.0 243 | 244 | ``` 245 | 246 | 具体的实现调用的代码如下,同样可以指定具体的存储数量以及预计的误判率: 247 | ```java 248 | import com.google.common.base.Charsets; 249 | import com.google.common.hash.BloomFilter; 250 | import com.google.common.hash.Funnels; 251 | 252 | public class GuavaBloomFilter { 253 | public static void main(String[] args) { 254 | BloomFilter bloomFilter = BloomFilter.create( 255 | Funnels.stringFunnel(Charsets.UTF_8),1000000,0.04); 256 | 257 | bloomFilter.put("Sam"); 258 | 259 | System.out.println(bloomFilter.mightContain("Jane")); 260 | System.out.println(bloomFilter.mightContain("Sam")); 261 | } 262 | } 263 | ``` 264 | 265 | 执行的结果如下,符合预期 266 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210314234457.png) 267 | 268 | 上面三种分别是手写,`redis`,`guava`实践了布隆过滤器,只是简单的用法,其实`redis`和`guava`里面的实现也可以看看,有兴趣可以了解一下,我先立一个`Flag`。 -------------------------------------------------------------------------------- /架构设计/布隆过滤器详解.md: -------------------------------------------------------------------------------- 1 | 前面我们提到,在防止缓存穿透的情况(缓存穿透是指,**缓存和数据库都没有的数据**,被大量请求,比如订单号不可能为`-1`,但是用户请求了大量订单号为`-1`的数据,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到数据库。),我们可以考虑使用布隆过滤器,来过滤掉绝对不存于集合中的元素。 2 | 3 | 4 | 5 | ## 布隆过滤器是什么呢? 6 | 布隆过滤器(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的,它实际上是由一个很长的二进制向量和一系列随机hash映射函数组成(说白了,就是用二进制数组存储数据的特征)。可以使用它来判断一个元素是否存在于集合中,它的优点在于查询效率高,空间小,缺点是存在一定的误差,以及我们想要剔除元素的时候,可能会相互影响。 7 | 8 | 也就是当一个元素被加入集合的时候,通过多个hash函数,将元素映射到位数组中的k个点,置为1。 9 | 10 | ## 为什么需要布隆过滤器? 11 | 一般情况下,我们想要判断是否存在某个元素,一开始考虑肯定是使用数组,但是使用数组的情况,查找的时候效率比较慢,要判断一个元素不存在于数组中,需要每次遍历完所有的元素。删除完一个元素后,还得把后面的其他元素往前面移动。 12 | 13 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308225741.png) 14 | 15 | 其实可以考虑使用`hash`表,如果有`hash`表来存储,将是以下的结构: 16 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308230509.png) 17 | 18 | 但是这种结构,虽然满足了大部分的需求,可能存在两点缺陷: 19 | - 只有一个hash函数,其实两个元素hash到一块,也就是产生hash冲突的可能性,还是比较高的。虽然可以用拉链法(后面跟着一个链表)的方式解决,但是操作时间复杂度可能有所升高。 20 | - 存储的时候,我们需要把元素引用给存储进去,要是上亿的数据,我们要将上亿的数据存储到一个hash表里面,不太建议这样操作。 21 | 22 | 对于上面存在的缺陷,其实我们可以考虑,用多个hash函数来减少冲突(注意:冲突时不可以避免的,只能减少),用位来存储每一个hash值。这样既可以减少hash冲突,还可以减少存储空间。 23 | 24 | 假设有三个hash函数,那么不同的元素,都会使用三个hash函数,hash到三个位置上。 25 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308233116.png) 26 | 27 | 假设后面又来了一个张三,那么在hash的时候,同样会hash到以下位置,所有位都是1,我们就可以说张三已经存在在里面了。 28 | 29 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308233607.png) 30 | 31 | 32 | 那么有没有可能出现误判的情况呢?这是有可能的,比如现在只有张三,李四,王五,蔡八,hash映射值如下: 33 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308233752.png) 34 | 35 | 后面来了陈六,但是不凑巧的是,它hash的三个函数hash出来的位,刚刚好就是被别的元素hash之后,改成1了,判断它已经存在了,但是实际上,陈六之前是不存在的。 36 | 37 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210308233900.png) 38 | 39 | 上面的情况,就是误判,布隆过滤器都会不可避免的出现误判。但是它有一个好处是,**布隆过滤器,判断存在的元素,可能不存在,但是判断不存在的元素,一定不存在。**,因为判断不存在说明至少有一位hash出来是对不上的。 40 | 41 | 42 | 也是由于会出现多个元素可能hash到一起,但有一个数据被踢出了集合,我们想把它映射的位,置为0,相当于删除该数据。这个时候,就会影响到其他的元素,可能会把别的元素映射的位,置为了0。这也就是为什么布隆过滤器不能删除的原因。 43 | 44 | ## 具体步骤 45 | 46 | 添加元素: 47 | - 1. 使用多个hash函数对元素item进行hash运算,得到多个hash值。 48 | - 2. 每一个hash值对bit位数组取模,得到位数组中的位置索引index。 49 | - 3. 如果index的位置不为1,那么就将该位置为1。 50 | 51 | 判断元素是否存在: 52 | - 1. 使用多个hash函数对元素item进行hash运算,得到多个hash值。 53 | - 2. 每一个hash值对bit位数组取模,得到位数组中的位置索引index。 54 | - 3. 如果index所处的位置都为1,说明元素可能已经存在了。 55 | 56 | ## 误判率推导 57 | 庆幸的是,布隆过滤器的误判率是可以预测的,由上面的分析,也可以得知,其实是与位数组的大小,以及hash函数的个数等,这些都是息息相关的。 58 | 59 | 假设位数组的大小是m,我们一共有k个hash函数,那么每一个hash函数,进行hash的时候,只能hash到m位中的一个位置,所以没有被hash到的概率是: 60 | $$1-\frac{1}{m}$$ 61 | 62 | k个hash函数都hash之后,该位还是没有被hash到1的概率是: 63 | $$(1-\frac{1}{m})^k$$ 64 | 65 | 如果我们插入了n个元素,也就是hash了n*k次,该位还是没有被hash成1的概率是: 66 | $$(1-\frac{1}{m})^{kn}$$ 67 | 68 | 那该位为1的概率就是: 69 | $$1-(1-\frac{1}{m})^{kn}$$ 70 | 71 | 如果需要检测某一个元素是不是在集合中,也就是该元素对应的k个hash元素hash出来的值,都需要设置为1。也就是该元素不存在,但是该元素对应的所有位都被hash成为1的概率是: 72 | $${(1-(1-\frac{1}{m})^{kn})}^{k}\approx {(1-e^{-kn/m})}^k $$ 73 | 74 | 可以大致看出,随着位数组大小m和hash函数个数的增加,其实概率会下降,随着插入的元素n的增加,概率会有所上升。 75 | 76 | 最后也可以通过自己期待的误判率P和期待添加的个数n,来大致计算出布隆过滤器的位数组的长度: 77 | $$m=-(\frac{nInP}{(In2)^2})$$ 78 | 79 | 上面就是误判率的大致计算方式,同时也提示我们,可以根据自己业务的数据量以及误判率,来调整我们的数组的大小。 80 | 81 | ## 布隆过滤器的作用 82 | 83 | 除了我们前面说的过滤爬虫恶意请求,还可以对一些URL进行去重,过滤海量数据里面的重复数据,过滤数据库里面不存在的id等等。 84 | 85 | 但是,即使有布隆过滤器,我们也不可能完全避免,或者彻底解决缓存穿透这个问题。只是相当于做了优化,将准确率提高。 86 | 87 | 很多的key-value数据库也会使用布隆过滤器来加快查询效率,因为全部挨个判断一遍,这个效率太低了。 88 | 89 | -------------------------------------------------------------------------------- /架构设计/并发中分布式锁setnx解析.md: -------------------------------------------------------------------------------- 1 | 前面讲解到,如果出现网络延迟的情况下,多个请求阻塞,那么恶意攻击就可以全部请求领取接口成功,而针对这种做法,我们使用`setnx`来解决,确保只有一个请求可以进入接口请求。 2 | 3 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210226230957.png) 4 | 5 | ```java 6 | public String receiveGitf(int activityId,int giftId,String uid){ 7 | // isExist判断活动是否存在,内部包括redis和数据库请求,省略 8 | if(isActivityExist(activityId,giftId)){ 9 | // 活动和礼品有效,判断是否领取过 10 | if(!userReceived(uid,activityId,giftId)){ 11 | // 没有领取过,调用C系统 12 | try { 13 | // setnx 14 | if(redis.setnx("uid_activityId_giftId")){ 15 | boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift"); 16 | if(receivedResult){ 17 | // 领取成功更新mysql 18 | updateMysql(uid,activityId,giftId); 19 | }else{ 20 | // 领取成功更新redis 21 | deleteRedis(uid,activityId,giftId); 22 | return "已经领过/领取失败"; 23 | } 24 | }else{ 25 | return "已经领过/领取失败"; 26 | } 27 | }catch (Exception e){ 28 | // 记录日志 29 | logHelper.log(e); 30 | return "调用领券系统失败,请重试"; 31 | } 32 | } 33 | } 34 | return "领取失败,活动不存在"; 35 | } 36 | ``` 37 | 38 | 下面,我们就专门讲解一下`setnx`,`setnx`可以用作分布式锁,但是**这个场景并不是分布式锁的一个较好的实践,因为每个用户的key都是不一样的,我们主要是防止同一个用户恶意领取**,`setnx`本身是一个原子操作,可以保证多个线程只有一个能拿到锁,能返回`true`,其他的都会返回`false`。 39 | 40 | 但是上面的做法,没有设置过期时间,在生产上一般是不可以这么使用。**不设置过期时间的key多了之后,redis服务器很容易内存打满,这时候不知道哪些是强制依赖的,只能扩容,从代码层面去清理,如果直接清理不常用的,也很难保证不出事。**(基本不允许这么干,除非是基础数据,跟着服务器启动,写入`redis`的,不会变更的,比如城市数据,国家数据等等,当然,这些也可以考虑在本地内存中实现) 41 | 42 | 如果在上面的代码中,加入超时时间,假设是一个月或者半年,流程变成这样: 43 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210228165201.png) 44 | 45 | 设置key的超时时间使用`expire`,但是这样还有缺陷么? 46 | 47 | 在`redis 2.6.12`之前,`setnx`和`expire`都不是原子操作,也就是很有可能在`setnx`成功之后,redis当季,expire设置失败,也就不会有超时时间了。虽然这个影响在当前业务不是很大,但是还是一个小缺陷。 48 | 49 | `Redis2.6.12`以上版本,可以用`set`获取锁,set包含`setnx`和`expire`,实现了原子操作。也就是两步要么一起成功,要么一起失败。 50 | 51 | 除此之外,上面的流程可能还存在的一个问题,是请求`C`服务的时候出现超时,然后删除key,恰好这个时候`redis`有问题,删除失败了,这个`key`就永远存在了。表现在业务上,就是`A`用户点击了领取,领取失败了,但是后面再怎么点,都是已经领取的状态了。 52 | 53 | **那这种现象怎么优化呢?** 54 | 55 | 这种情况,其实已经是很少见的情况,按照我们当前的业务场景也看,就是当前的用户,`redis`记录了它已经领取过了,但是由于接口的失败,成功之后还没将`mysql/其他数据库`更新,两个数据库不一致了。 56 | 57 | 我能想到的一个方法,就是再删除失败的时候,告警,并且将业务相关的数据记录下来,比如`key`,`uid`等等,针对这部分数据,做一次补发,或者手动删除key。 58 | 59 | 或者,启动一个定时任务或者`lua`脚本,去判定`redis`和数据库不一致的情况,但是切记不要全部查询,应该是隔一段时间,查询最后增加的部分,做一个校验以及相应的处理。枚举`key`是十分耗时的操作!!! 60 | 61 | `setnx` 除了解决上面的问题,还可以应用在解决**缓存击穿**的问题上。 62 | 63 | 譬如现在有热点数据,不仅在`mysql`数据库存储了,还在`redis`中存了一份缓存,那么如果有一个时间点,缓存失效了,这时候,大量的请求打过来,同时到达,缓存拿不到数据,都去数据库取数据,假设数据库操作比较耗时,那么压力全都在数据库服务器上了。 64 | 65 | 这个时候所有的请求都去更新数据,明显是不合适的,应该是使用分布式锁,让一个线程去请求`mysql`一次即可。但是为了避免死锁的情况,如果超时,得及时额外释放锁,要不可能请求`mysql`都失败了,其他线程又拿不到锁,那么数据就会一直为`null`了。 66 | 67 | 可以使用以下的命令: 68 | ```shell 69 | SETNX lock.foo 70 | ``` 71 | 72 | 关于这个场景下的`setnx`先讲到这里,后面再讲讲分布式锁相关的知识。 73 | 74 | -------------------------------------------------------------------------------- /架构设计/海量ip中查找出现次数最多的一个.md: -------------------------------------------------------------------------------- 1 | ## 场景题 2 | 3 | > 有 100 机器,每个机器的磁盘特别大,磁盘大小为 1T,但是内存大小只有 4G,现在每台机器上都产生了很多 ip 日志文件,每个文件假设有50G,那么如果计算出这 100 太机器上访问量最多的 100 ip 呢?也就是Top 100。 4 | 5 | 6 | 7 | ## 思路 8 | 9 | 其实,一开始我有往布隆过滤器那边考虑,但是布隆过滤器只能大致的判断一个 ip 是否已经存在,而不能去统计数量,不符合该场景。 10 | 11 | 那么一般这种大数据的问题,都是因为一次不能完全加载到内存,因此需要拆分,那怎么拆呢?ip是32位的,也就是最多就 232 个, 常见的拆分方法都是 `哈希`: 12 | 13 | - 把大文件通过哈希算法分配到不同的机器 14 | - 把大文件通过哈希算法分配到不同的小文件 15 | 16 | 上面所说,一台机器的内存肯定不能把所有的 ip 全部加载进去,必须在不同机器上先 hash 区分,先看每台机器上,50G 文件,假设我们分成 100 个小文件,那么平均每个就500M,使用 Hash 函数将所有的 ip 分流到不同的文件中。 17 | 18 | 这个时候相同的 ip 一定在相同的文件中,当然不能排除数据全部倾斜于一个文件的情况,也就是虽然 hash了,但是由于个别ip或者hash值相同的ip太多了,都分到了个别文件上,那么这个时候分流后的文件依旧很大。这种情况我能想到的就是要是文件还是很大,需要再hash,如果基本属于同一个ip,那么这个时候就可以分批次读取,比如一次只读 1G 到内存。 19 | 20 | 在处理每个小文件时,使用 HashMap 来统计每个 ip 出现的频率,统计完成后,遍历,用最小根堆,获取出现频率最大的100个ip。这个时候,每个小文件都获取到了出现频率最大的100个 ip,然后每个文件的 Top 100 个ip 再进行==排序==即可(每个文件的top100 都是不一样的,因为前面进行 hash 之后保证相同的 ip 只会落到同一个文件里)。这样就可以得到每台机器上的 Top 100。 21 | 22 | 不同机器的 Top 100 再进行 `加和` 并 `排序`,就可以得到Top 100 的ip。 23 | 24 | 为什么加和? 因为不同机器上可能存在同样的ip,前面的hash操作只是确保同一个机器的不同文件里面的ip一定不一样。 25 | 26 | 27 | 28 | **但是上面的操作有什么瑕疵么?当然有!** 29 | 30 | 假设我们又两台机器,有一台机器 `C1` 的top 100 的ip是 `192.128.1.1`,top 101 是 `192.128.1.2`,那么就可能存在另一台机器 `C2` 上 `192.128.1.1` 可能从来没有出现过,但是 `192.128.1.2` 却也排在 top 101,其实总数上 `192.128.1.2` 是超过`192.128.1.1`,但是很不幸的是,我们每台机器只保存了 top100,所以它在计算过程中被淘汰了,导致结果不准确。 31 | 32 | 33 | 34 | **解决方案:** 35 | 36 | 先用 hash 算法,把 ip 按照 hash 值哈希到不同的机器上,保证相同的ip在相同的机器上,再对每个机器上的ip文件再hash成小文件,这个时候再分别统计小文件的出现频次,用最小根堆处理,不同文件的结果排序,就可以得到每台机器的top 100,再进行不同机器之间的结果排序,就可以得到真正的 top 100。 37 | 38 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210919230023.png) 39 | 40 | **** 41 | 42 | 43 | 44 | 一般而言,像这种海量数据,比如 `有一个包含100亿个URL的大文件,假设每个URL占用64B,请找出其中所有重复的URL.` ,内存一次性读不下,只能通过 ==分而治之==。 45 | 46 | hash 到不同的小文件,一直这样划分,直到满足资源的限制: 47 | 48 | - hash分流 49 | - hash表统计 50 | - 最小堆/外部排序 51 | 52 | 53 | 54 | 如果允许一定的误差存在,其实还可以考虑使用布隆过滤器(Bloom filter),将URL挨个映射到每一个Bit,在此之前判断该位置是否映射过,来证明它是否已经存在。(`有一定的概率出现误判,因为其他的URL也可能会映射到同一位置`) -------------------------------------------------------------------------------- /架构设计/设计一个短链接生成系统.md: -------------------------------------------------------------------------------- 1 | ## 引言 2 | 3 | 相信大家在生活中,特别是最近的双十一活动期间,会收到很多短信,而那些短信都有两个特征,第一个是几乎都是垃圾短信,这个特点此处可以忽略不计,第二个特点是**链接很短**,比如下面这个: 4 | 5 | 6 | 7 | 我们知道,短信有些是有字数限制的,直接放一个带满各种参数的链接,不合适,另外一点是,不想暴露参数。好处无非以下: 8 | 9 | - 太长的链接容易被限制长度 10 | - 短链接看着简洁,长链接看着容易懵 11 | - 安全,不想暴露参数 12 | - 可以统一链接转换,当然也可以实现统计点击次数等操作 13 | 14 | 那背后的原理是什么呢?怎么实现的?让你实现这样的系统,你会怎么设计呢?【来自于某鹅场面试官】 15 | 16 | 17 | 18 | ## 短链接的原理 19 | 20 | ### 短链接展示的逻辑 21 | 22 | 这里最重要的知识点是重定向,先复习一下`http`的状态码: 23 | 24 | | 分类 | 含义 | 25 | | :--- | :--------------------------------------------- | 26 | | 1** | 服务器收到请求,需要请求者继续执行操作 | 27 | | 2** | 成功,操作被成功接收并处理 | 28 | | 3** | 重定向,需要进一步的操作以完成请求 | 29 | | 4** | 客户端错误,请求包含语法错误或无法完成请求 | 30 | | 5** | 服务器错误,服务器在处理请求的过程中发生了错误 | 31 | 32 | 那么以 3 开头的状态码都是关于重定向的: 33 | 34 | - 300:多种选择,可以在多个位置存在 35 | - 301:永久重定向,浏览器会缓存,自动重定向到新的地址 36 | - 302:临时重定向,客户端还是会继续使用旧的URL 37 | - 303:查看其他的地址,类似于301 38 | - 304:未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。 39 | - 305:需要使用代理才能访问到资源 40 | - 306:废弃的状态码 41 | - 307:临时重定向,使用Get请求重定向 42 | 43 | 44 | 45 | 整个跳转的流程: 46 | 47 | - 1.用户访问短链接,请求到达服务器 48 | - 2.服务器将短链接装换成为长链接,然后给浏览器返回重定向的状态码301/302 49 | - 301永久重定向会导致浏览器缓存重定向地址,短链接系统统计访问次数会不正确 50 | - 302临时重定向可以解决次数不准的问题,但是每次都会到短链接系统转换,服务器压力会变大。 51 | - 3.浏览器拿到重定向的状态码,以及真正需要访问的地址,重定向到真正的长链接上。 52 | 53 | 54 | 55 | 从下图可以看出,确实链接被`302`重定向到新的地址上去,返回的头里面有一个字段`Location`就是所要重定向的地址: 56 | 57 | 58 | 59 | ### 短链接怎么设计的? 60 | 61 | #### 全局发号器 62 | 63 | 肯定我们第一点想到的是压缩,像文件压缩那样,压缩之后再解压还原到原来的链接,重定向到原来的链接,但是很不幸的是,这个是行不通的,你有见过什么压缩方式能把这么长的数字直接压缩到这么短么?事实上不可能。就像是`Huffman`树,也只能对那种重复字符较多的字符串压缩时效率较高,像链接这种,可能带很多参数,而且各种不规则的情况都有,直接压缩算法不现实。 64 | 65 | 那`https://dx.10086.cn/tzHLFw`与`https://gd.10086.cn/gmccapp/webpage/payPhonemoney/index.html?channel=`之间的装换是怎么样的呢?前面路径不变,变化的是后面,也就是`tzHLFw`与`gmccapp/webpage/payPhonemoney/index.html?channel=`之间的转换。 66 | 67 | 实际也很简单,就是数据库里面的一条数据,一个`id`对应长链接(相当于全局的发号器,全局唯一的ID): 68 | 69 | | id | url | 70 | | :--: | :----------------------------------------------------------: | 71 | | 1 | https://gd.10086.cn/gmccapp/webpage/payPhonemoney/index.html?channel= | 72 | 73 | 这里用到的,也就是我们之前说过的分布式全局唯一ID,如果我们直接用`id`作为参数,貌似也可以:`https://dx.10086.cn/1`,访问这个链接时,去数据库查询获得真正的url,再重定向。 74 | 75 | 单机的唯一`ID`很简单,用原子类`AtomicLong`就可以,但是分布式的就不行了,简单点可以用 `redis`,或者数据库自增,或者可以考虑`Zookeeper`之类的。 76 | 77 | #### id 转换策略 78 | 79 | 但是直接用递增的数字,有两个坏处: 80 | 81 | - 数字很大的时候,还是很长 82 | - 递增的数字,不安全,规律性太强了 83 | 84 | 明显我们平时看到的链接也不是数字的,一般都是大小写字母加上数字。为了缩短链接的长度,我们必须把`id`转换掉,比如我们的短链接由`a-z`,`A-Z`,`0-9`组成,相当于`62`进制的数字,将`id`转换成为`62`进制的数字: 85 | 86 | ```java 87 | public class ShortUrl { 88 | 89 | private static final String BASE = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 90 | 91 | public static String toBase62(long num) { 92 | StringBuilder result = new StringBuilder(); 93 | do { 94 | int i = (int) (num % 62); 95 | result.append(BASE.charAt(i)); 96 | num /= 62; 97 | } while (num > 0); 98 | 99 | return result.reverse().toString(); 100 | } 101 | 102 | public static long toBase10(String str) { 103 | long result = 0; 104 | for (int i = 0; i < str.length(); i++) { 105 | result = result * 62 + BASE.indexOf(str.charAt(i)); 106 | } 107 | return result; 108 | } 109 | 110 | public static void main(String[] args) { 111 | // tzHLFw 112 | System.out.println(toBase10("tzHLFw")); 113 | System.out.println(toBase62(27095455234L)); 114 | } 115 | } 116 | ``` 117 | 118 | `id`转 `62`位的`key` 或者`key`装换成为`id`都已经实现了,不过计算还是比较耗时的,不如加个字段存起来,于是数据库变成了: 119 | 120 | | id | key | url | 121 | | :---------: | :----: | :----------------------------------------------------------: | 122 | | 27095455234 | tzHLFw | https://gd.10086.cn/gmccapp/webpage/payPhonemoney/index.html?channel= | 123 | 124 | 但是这样还是很容易被猜出这个`id`和`key`的对应关系,要是被遍历访问,那还是很不安全的,如果担心,可以随机将短链接的字符顺序打乱,或者在适当的位置加上一些随机生成的字符,比如第`1,4,5 `位是随机字符,其他位置不变,只要我们计算的时候,将它对应的关系存到数据库,我们就可以通过连接的`key`找到对应的`url`。(值得注意的是,`key`必须是全局唯一的,如果冲突,必须重新生成) 125 | 126 | 一般短链接都有过期时间,那么我们也必须在数据库里面加上对应的字段,访问的时候,先判断是否过期,过期则不给予重定向。 127 | 128 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211115003828.png) 129 | 130 | #### 性能考虑 131 | 132 | 如果有很多短链接暴露出去了,数据库里面数据很多,这个时候可以考虑使用缓存优化,生成的时候顺便把缓存写入,然后读取的时候,走缓存即可,因为一般短链接和长链接的关系不会修改,即使修改,也是很低频的事情。 133 | 134 | 如果系统的`id`用完了怎么办?这种概率很小,如果真的发生,可以重用旧的已经失效的`id`号。 135 | 136 | 如果被人疯狂请求一些不存在的短链接怎么办?其实这就是缓存穿透,缓存穿透是指,**缓存和数据库都没有的数据**,被大量请求,比如订单号不可能为`-1`,但是用户请求了大量订单号为`-1`的数据,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到数据库。如果被恶意用户利用,疯狂请求不存在的数据,就会导致数据库压力过大,甚至垮掉。 137 | 138 | 针对这种情况,一般可以用布隆过滤器过滤掉不存在的数据请求,但是我们这里`id`本来就是递增且有序的,其实我们范围大致都是已知的,更加容易判断,超出的肯定不存在,或者请求到的时候,缓存里面放一个空对象也是没有问题的。 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /架构设计/设计领取礼品的架构以及并发问题解决.md: -------------------------------------------------------------------------------- 1 | 现在 有一个场景,领取礼品,每个用户有次数限制,用户通过前端点击,调用了应用A的接口,里面调用了服务B,服务B里面去调用了服务C,注意服务C是其他部门的服务。服务C负责真正的发放礼品。(假设这个服务C我们是不可修改的,A,B是自己团队负责的,并且可能出现高并发的情况) 2 | 3 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210226212257.png) 4 | 5 | 我们应该如何做这个次数限制呢? 6 | 7 | 假设每次领取礼品的活动有一个`activityId`,一个用户一个活动可以领取一件礼品,礼品有`giftId`,不可以多领,每个用户对应一个`uid`。 8 | 9 | 10 | ## 查询是否可以领取 11 | 首先对于前端而言,进入系统,首先需要获取用户是否已经领取过,而这个是否已经领取过,具体的实现我们应该写在B服务中,用户通过应用A,请求到服务B,返回用户是否已经领取的结果。 12 | 13 | 查询是否领取的流程大致如下: 14 | 用户进入页面,前端如果有缓存的话,可以为他展示之前缓存的结果,假设没有缓存,就会请求A应用,A应用会去请求B服务,B服务首先需要判断礼品或者活动是否存在。 15 | 16 | 去redis里面取活动或者礼品是否存在,如果redis没有查询到,那么就查询数据库,返回结果,如果数据库都没有,说明这个前端请求很可能是捏造的,直接返回结果“活动或者礼品不存在”,如果此时查询出来,确实存在,那么就需要去查询是否领取过,同样是查询redis,不存在的情况下,查询数据库,再返回结果。,如果领取过,则会有领取结果,前端将按键置灰,否者用户按键可以领取。 17 | 18 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210226214848.png) 19 | 20 | 上面的redis肯定是需要我们维护的,这里不展开讲。比如增加活动的时候,除了改数据库,同时需要`redis`里面写一份数据,key可以是`activityId_giftId`,记录已经有的活动,用户成功领取的时候,同样是不仅增加数据库记录,也需要往`redis`写一份数据,key可以是`activityId_giftId_uid`,记录该用户已经领取过该活动的奖品。 21 | 22 | 但是上面的系统,有一个问题,就是活动/礼品不存在的时候,请求会每一次都直接打到数据库,如果是恶意攻击,数据库就挂了。这里当然可以考虑使用布隆过滤器,对请求参数中的活动/礼品做过滤,同时也可以考虑其他的防爬虫手段,比如滑动窗口统计请求数,根据`ip`,客户端`id`,`uid`等等。 23 | 24 | 当然,如果可以保证`redis`数据可靠,稳定,可以不请求数据库,`redis`不包含则说明不存在,直接返回。但是这种做法需要在增加活动/修改商品的时候,同时将`redis`一同修改同步。如果redis挂掉的情况,或者请求`redis`异常,再去查询数据库。如果能接受修改数据库活动信息不立马更新,也可以考虑更新完数据库,用消息队列发一条消息,收到再做`redis`更新。当然,这个不是一种好的做法,解耦合之后,增加了复杂度。前面说的做法,只要`redis`挂了,数据库理论上也支撑不了多久(极端情况)。 25 | 26 | (当然,上面不是完美的方案,是个大致流程) 27 | 28 | ## 领取礼品接口怎么处理? 29 | 30 | 首先流程上与上面的查询是否领取过有些类似,,但是在查询是否领取过这一步之后,有所不同。如果已经领取过,则直接返回,但是如果没有领取过,需要调用C服务进行领取,如果调用C接口失败,或者返回领取失败,B服务需要做的事,就是记录日志或者告警,同时返回失败。 31 | 如果C服务返回领取成功,那么需要记录领取记录到数据库,并且更新缓存,表示已经领取过该礼品,这也是上面为什么一般能直接查询缓存就可以知道用户是否领取过的原因。 32 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210226230957.png) 33 | 34 | 这个设计中,其实C服务才是真正实现方法奖品的服务,我们做的A和B相当于调用别人的服务,做了中间服务,这种情况更需要记录日志,控制爬虫,恶意攻击等等,同时做好异常处理。 35 | 36 | 上面的设计,如果我们来写段伪代码,来看看有什么问题? 37 | 38 | ```java 39 | public String receiveGitf(int activityId,int giftId,String uid){ 40 | // isExist判断活动是否存在,内部包括redis和数据库请求,省略 41 | if(isActivityExist(activityId,giftId)){ 42 | // 活动和礼品有效,判断是否领取过 43 | if(!userReceived(uid,activityId,giftId)){ 44 | // 没有领取过,调用C系统 45 | try { 46 | boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift"); 47 | if(receivedResult){ 48 | // 领取成功更新mysql 49 | updateMysql(uid,activityId,giftId); 50 | // 领取成功更新redis 51 | updateRedis(uid,activityId,giftId); 52 | }else{ 53 | return "已经领过/领取失败"; 54 | } 55 | }catch (Exception e){ 56 | // 记录日志 57 | logHelper.log(e); 58 | return "调用领券系统失败,请重试"; 59 | } 60 | } 61 | } 62 | return "领取失败,活动不存在"; 63 | } 64 | ``` 65 | 66 | 看起来好像没有什么问题,领取成功写`redis`,之后读到就不会再领取。但是高并发环境下呢?高并发环境下,很有可能出现领取多次的情况,因为网络请求不是瞬时可以返回的,如果有很多个同一个uid的请求,同时进来,C服务的处理或者延迟比较高。所有的请求都会堵塞在请求C服务这里。(网络请求需要时间!!!) 67 | 68 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210226233336.png) 69 | 70 | 这时候还没有任何请求成功,所以`redis`根本不会更新,数据库也不会,所以的请求都会打到C服务,假设别人的服务是不可靠的,可以多次领取,那么所有的请求都会成功,并且会有多条成功的记录!!! 71 | 72 | 那如何来改进这个问题呢? 73 | 我们可以使用`setnx`来处理,先请求`setnx`,更新缓存,然后只有一个可以成功进来,如果真的成功,再写数据库,如果异常或者请求失败,将缓存删除。 74 | 75 | ```java 76 | public String receiveGitf(int activityId,int giftId,String uid){ 77 | // isExist判断活动是否存在,内部包括redis和数据库请求,省略 78 | if(isActivityExist(activityId,giftId)){ 79 | // 活动和礼品有效,判断是否领取过 80 | if(!userReceived(uid,activityId,giftId)){ 81 | // 没有领取过,调用C系统 82 | try { 83 | // setnx 84 | if(redis.setnx("uid_activityId_giftId")){ 85 | boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift"); 86 | if(receivedResult){ 87 | // 领取成功更新mysql 88 | updateMysql(uid,activityId,giftId); 89 | }else{ 90 | // 领取成功更新redis 91 | deleteRedis(uid,activityId,giftId); 92 | return "已经领过/领取失败"; 93 | } 94 | }else{ 95 | return "已经领过/领取失败"; 96 | } 97 | }catch (Exception e){ 98 | // 记录日志 99 | logHelper.log(e); 100 | return "调用领券系统失败,请重试"; 101 | } 102 | } 103 | } 104 | return "领取失败,活动不存在"; 105 | } 106 | ``` 107 | 108 | 在 `Redis` 里,所谓 `SETNX`,是`「SET if Not eXists」`缩写,也就是只有`key`不存在的时候才设置,可以利用它来实现锁的效果。这样只有一个请求可以进入。 109 | ```shell 110 | 111 | redis> EXISTS id # id 不存在 112 | 113 | redis> SETNX id "1" # id 设置成功1 114 | 115 | redis> SETNX id "2" # 尝试覆盖 id ,返回失败 0 116 | 117 | redis> GET job # 没有被覆盖"2" 118 | ``` 119 | 120 | 这个场景下的问题已经得到初步的解决,那这个`setnx`有没有坑呢?下次我们聊一下... -------------------------------------------------------------------------------- /缓存/缓存穿透,缓存击穿,缓存雪崩的区别以及解决方案.md: -------------------------------------------------------------------------------- 1 | 平时我们使用缓存的方案,一般是在数据库中存储一份,在缓存中同步存储一份。当请求过来的视乎,可以先从缓存中取数据,如果有数据,直接返回缓存中的结果。如果缓存中没有数据,那么去数据库中取出数据,同时更新到缓存中,返回结果。如果数据库中也没有数据,可以直接返回空。 2 | 3 | 关于缓存,一般会有以下几个常见的问题 4 | 5 | ## 缓存穿透 6 | 缓存穿透是指,**缓存和数据库都没有的数据**,被大量请求,比如订单号不可能为`-1`,但是用户请求了大量订单号为`-1`的数据,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到数据库。 7 | 如果被恶意用户利用,疯狂请求不存在的数据,就会导致数据库压力过大,甚至垮掉。 8 | 9 | 注意:穿透的意思是,都没有,直接一路打到数据库。 10 | 11 | **那对于这种情况,我们该如何解决呢?** 12 | 13 | 1. 接口增加业务层级的`Filter`,进行合法校验,这可以有效拦截大部分不合法的请求。 14 | 2. 作为第一点的补充,最常见的是使用布隆过滤器,针对一个或者多个维度,把可能存在的数据值hash到bitmap中,bitmap证明该数据不存在则该数据一定不存在,但是bitmap证明该数据存在也只能是可能存在,因为不同的数值hash到的bit位很有可能是一样的,hash冲突会导致误判,多个hash方法也只能是降低冲突的概率,无法做到避免。 15 | 3. 另外一个常见的方法,则是针对数据库与缓存都没有的数据,对空的结果进行缓存,但是过期时间设置得较短,一般五分钟内。而这种数据,如果数据库有写入,或者更新,必须同时刷新缓存,否则会导致不一致的问题存在。 16 | 17 | 18 | ## 缓存击穿 19 | 缓存击穿是指数据库原本有得数据,但是缓存中没有,一般是缓存突然失效了,这时候如果有大量用户请求该数据,缓存没有则会去数据库请求,会引发数据库压力增大,可能会瞬间打垮。 20 | 21 | 针对这类问题,一般有以下做法: 22 | 1. 如果是热点数据,那么可以考虑设置永远不过期。 23 | 2. 如果数据一定会过期,那么就需要在数据为空的时候,设置一个互斥的锁,只让一个请求通过,只有一个请求去数据库拉取数据,取完数据,不管如何都需要释放锁,异常的时候也需要释放锁,要不其他线程会一直拿不到锁。 24 | 25 | 下面是缓存击穿的时候互斥锁的写法,注意:获取锁之后操作,不管成功或者失败,都应该释放锁,而其他的请求,如果没有获取到锁,应该等待,再重试。当然,如果是需要更加全面一点,应该加上一个等待次数,比如1s中,那么也就是睡眠五次,达到这个阈值,则直接返回空,不应该过度消耗机器,以免当个不可用的场景把整个应用的服务器带挂了。 26 | 27 | ```java 28 | public static String getProductDescById(String id) { 29 | String desc = redis.get(id); 30 | // 缓存为空,过期了 31 | if (desc == null) { 32 | // 互斥锁,只有一个请求可以成功 33 | if (redis.setnx(lock_id, 1, 60) == 1) { 34 | try { 35 | // 从数据库取出数据 36 | desc = getFromDB(id); 37 | redis.set(id, desc, 60 * 60 * 24); 38 | } catch (Exception ex) { 39 | LogHelper.error(ex); 40 | } finally { 41 | // 确保最后删除,释放锁 42 | redis.del(lock_id); 43 | return desc; 44 | } 45 | } else { 46 | // 否则睡眠200ms,接着获取锁 47 | Thread.sleep(200); 48 | return getProductDescById(id); 49 | } 50 | } 51 | } 52 | ``` 53 | ## 缓存雪崩 54 | 55 | 缓存雪崩是指缓存中有大量的数据,在同一个时间点,或者较短的时间段内,全部过期了,这个时候请求过来,缓存没有数据,都会请求数据库,则数据库的压力就会突增,扛不住就会宕机。 56 | 57 | 针对这种情况,一般我们都是使用以下方案: 58 | 1. 如果是热点数据,那么可以考虑设置永远不过期。 59 | 2. 缓存的过期时间除非比较严格,要不考虑设置一个波动随机值,比如理论十分钟,那这类key的缓存时间都加上一个1~3分钟,过期时间在7~13分钟内波动,有效防止都在同一个时间点上大量过期。 60 | 3. 方法1避免了有效过期的情况,但是要是所有的热点数据在一台redis服务器上,也是极其危险的,如果网络有问题,或者redis服务器挂了,那么所有的热点数据也会雪崩(查询不到),因此将热点数据打散分不到不同的机房中,也可以有效减少这种情况。 61 | 4. 也可以考虑双缓存的方式,数据库数据同步到缓存A和B,A设置过期时间,B不设置过期时间,如果A为空的时候去读B,同时异步去更新缓存,但是更新的时候需要同时更新两个缓存。 62 | 63 | 比如设置产品的缓存时间: 64 | ```java 65 | redis.set(id,value,60*60 + Math.random()*1000); 66 | ``` 67 | 68 | 69 | ## 小结 70 | 缓存穿透是指数据库原本就没有的数据,请求如入无人之境,直奔数据库,而缓存击穿,则是指数据库有数据,缓存也本应该有数据,但是突然缓存过期了,这层保护屏障被击穿了,请求直奔数据库,缓存雪崩则是指很多缓存同一个时间失效了,流量全部涌入数据库,造成数据库极大的压力。 -------------------------------------------------------------------------------- /设计模式/设计模式【1.1】-- 饿汉式单例为什么是线程安全的.md: -------------------------------------------------------------------------------- 1 | 我们都知道,饿汉式单例是线程安全的,也就是不会初始化的时候创建出两个对象来,但是为什么呢? 2 | 3 | 首先定义一个饿汉式单例如下: 4 | 5 | ```java 6 | public class Singleton { 7 | // 私有化构造方法,以防止外界使用该构造方法创建新的实例 8 | private Singleton(){ 9 | } 10 | // 默认是public,访问可以直接通过Singleton.instance来访问 11 | static Singleton instance = new Singleton(); 12 | } 13 | ``` 14 | 15 | 之所以是线程安全的,是因为JVM在类加载的过程,保证了不会初始化多个`static`对象。类的生命周期主要是: 16 | 17 | **加载**-->**验证**-->**准备**-->**解析**-->**初始化**-->**使用**-->**卸载** 18 | 19 | 上面的代码,实际上类成员变量`instance`是在初始化阶段的时候完成初始化,所有的类变量以及`static`静态代码块,都是在一个叫`clinit()`的方法里面完成初始化。这一点,使用`jclasslib`可以看出来: 20 | 21 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201216211724.png) 22 | 23 | 24 | 25 | `clinit()`方法是由虚拟机收集的,包含了`static`变量的**赋值操作**以及`static`代码块,所以我们代码中的`static Singleton instance = new Singleton();`就是在其中。虚拟机本身会保证`clinit()`代码在多线程并发的时候,只会有一个线程可以访问到,其他的线程都需要等待,并且等到执行的线程结束后才可以接着执行,但是它们不会再进入`clinit()`方法,所以是线程安全的。我们可以验证一下: 26 | 27 | 首先改造一下单例: 28 | 29 | ```java 30 | public class Singleton { 31 | // 私有化构造方法,以防止外界使用该构造方法创建新的实例 32 | private Singleton() { 33 | } 34 | 35 | // 默认是public,访问可以直接通过Singleton.instance来访问 36 | static Singleton instance = null; 37 | 38 | static { 39 | System.out.println("初始化static模块---开始"); 40 | instance = new Singleton(); 41 | try { 42 | System.out.println("初始化中..."); 43 | Thread.sleep(20 * 1000); 44 | } catch (InterruptedException e) { 45 | e.printStackTrace(); 46 | } 47 | System.out.println("初始化static模块----结束"); 48 | } 49 | } 50 | ``` 51 | 52 | 测试代码: 53 | 54 | ```java 55 | import java.io.*; 56 | import java.lang.reflect.InvocationTargetException; 57 | 58 | public class SingletonTests { 59 | public static void main(String[] args) throws Exception, InvocationTargetException, InstantiationException, NoSuchMethodException { 60 | Thread thread1 = new Thread(new Runnable() { 61 | public void run() { 62 | System.out.println("线程1开始尝试初始化单例"); 63 | Singleton singleton = Singleton.instance; 64 | System.out.println("线程1获取到的单例:" + singleton); 65 | } 66 | }); 67 | Thread thread2 = new Thread(new Runnable() { 68 | public void run() { 69 | System.out.println("线程2开始尝试初始化单例"); 70 | Singleton singleton = Singleton.instance; 71 | System.out.println("线程2获取到的单例:" + singleton); 72 | } 73 | }); 74 | thread1.start(); 75 | thread2.start(); 76 | } 77 | } 78 | ``` 79 | 80 | 运行结果,一开始运行的时候,我们可以看到线程1进去了`static`代码块,它在初始化,线程2则在等待。 81 | 82 | ![image-20201217141915904](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20201217141915904.png) 83 | 84 | 待到线程1初始化完成的时候,线程2也不会再进入`static`代码块,而是和线程1取得同一个对象,由此可见,`static`代码块实际上就是线程安全的。 85 | 86 | ![image-20201217143603156](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20201217143603156.png) 87 | 88 | -------------------------------------------------------------------------------- /设计模式/设计模式【1】--设计模式之单例.md: -------------------------------------------------------------------------------- 1 | 单例模式,是一种比较简单的设计模式,也是属于创建型模式(提供一种创建对象的模式或者方式)。 2 | 要点: 3 | 4 | - 1.涉及一个单一的类,这个类来创建自己的对象(不能在其他地方重写创建方法,初始化类的时候创建或者提供私有的方法进行访问或者创建,必须确保只有单个的对象被创建)。 5 | - 2.单例模式不一定是线程不安全的。 6 | - 3.单例模式可以分为两种:**懒汉模式**(在第一次使用类的时候才创建,可以理解为类加载的时候特别懒,要用的时候才去获取,要是没有就创建,由于是单例,所以只有第一次使用的时候没有,创建后就可以一直用同一个对象),**饿汉模式**(在类加载的时候就已经创建,可以理解为饿汉已经饿得饥渴难耐,肯定先把资源紧紧拽在自己手中,所以在类加载的时候就会先创建实例) 7 | 8 | > 关键字: 9 | > * 单例:`singleton` 10 | > * 实例:`instance` 11 | > * 同步: `synchronized` 12 | 13 | 14 | ## 饿汉模式 15 | ### 1.私有属性 16 | 第一种`single`是`public`,可以直接通过`Singleton`类名来访问。 17 | ```java 18 | public class Singleton { 19 | // 私有化构造方法,以防止外界使用该构造方法创建新的实例 20 | private Singleton(){ 21 | } 22 | // 默认是public,访问可以直接通过Singleton.instance来访问 23 | static Singleton instance = new Singleton(); 24 | } 25 | ``` 26 | 27 | ### 2.公有属性 28 | 第二种是用`private`修饰`singleton`,那么就需要提供`static` 方法来访问。 29 | ```java 30 | public class Singleton { 31 | private Singleton(){ 32 | } 33 | // 使用private修饰,那么就需要提供get方法供外界访问 34 | private static Singleton instance = new Singleton(); 35 | // static将方法归类所有,直接通过类名来访问 36 | public static Singleton getInstance(){ 37 | return instance;. 38 | } 39 | } 40 | ``` 41 | 42 | ### 3. 懒加载 43 | 饿汉模式,这样的写法是没有问题的,不会有线程安全问题(类的`static`成员创建的时候默认是上锁的,不会同时被多个线程获取到),但是是有缺点的,因为`instance`的初始化是在类加载的时候就在进行的,所以类加载是由`ClassLoader`来实现的,那么初始化得比较早好处是后来直接可以用,坏处也就是浪费了资源,要是只是个别类使用这样的方法,依赖的数据量比较少,那么这样的方法也是一种比较好的单例方法。 44 | 在单例模式中一般是调用`getInstance()`方法来触发类装载,以上的两种饿汉模式显然没有实现`lazyload`(个人理解是用的时候才触发类加载) 45 | 所以下面有一种饿汉模式的改进版,利用内部类实现懒加载。 46 | 这种方式`Singleton类`被加载了,但是`instance`也不一定被初始化,要等到`SingletonHolder`被主动使用的时候,也就是显式调用`getInstance()`方法的时候,才会显式的装载`SingletonHolder`类,从而实例化`instance`。这种方法使用类装载器保证了只有一个线程能够初始化`instance`,那么也就保证了单例,并且实现了懒加载。 47 | 48 | 值得注意的是:**静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。** 49 | 50 | ```java 51 | public class Singleton { 52 | private Singleton(){ 53 | } 54 | //内部类 55 | private static class SingletonHolder{ 56 | private static final Singleton instance = new Singleton(); 57 | } 58 | //对外提供的不允许重写的获取方法 59 | public static final Singleton getInstance(){ 60 | return SingletonHolder.instance; 61 | } 62 | } 63 | ``` 64 | 65 | ## 懒汉模式 66 | 最基础的代码**(线程不安全)**: 67 | ```java 68 | public class Singleton { 69 | private static Singleton instance = null; 70 | private Singleton(){ 71 | } 72 | public static Singleton getInstance() { 73 | if (instance == null) { 74 | instance = new Singleton(); 75 | } 76 | return instance; 77 | } 78 | } 79 | ``` 80 | 这种写法,是在每次获取实例`instance`的时候进行判断,如果没有那么就会`new`一个出来,否则就直接返回之前已经存在的`instance`。但是这样的写法**不是线程安全的**,当有多个线程都执行`getInstance()`方法的时候,都判断是否等于null的时候,就会各自创建新的实例,这样就不能保证单例了。所以我们就会想到同步锁,使用**synchronized**关键字: 81 | 加同步锁的代码**(线程安全,效率不高)** 82 | ```java 83 | public class Singleton { 84 | private static Singleton instance = null; 85 | private Singleton() {} 86 | public static Singleton getInstance() { 87 | synchronized(Singleton.class){ 88 | if (instance == null) 89 | instance = new Singleton(); 90 | } 91 | return instance; 92 | } 93 | } 94 | ``` 95 | 这样的话,`getInstance()`方法就会被锁上,当有两个线程同时访问这个方法的时候,总会有一个线程先获得了同步锁,那么这个线程就可以执行下去,而另一个线程就必须等待,等待第一个线程执行完`getInstance()`方法之后,才可以执行。这段代码**是线程安全的**,但是效率不高,因为假如有很多线程,那么就必须让所有的都等待正在访问的线程,这样就会**大大降低了效率**。那么我们有一种思路就是,将锁出现等待的概率再降低,也就是我们所说的双重校验锁(双检锁)。 96 | ```java 97 | public class Singleton { 98 | private static Singleton instance = null; 99 | private Singleton() {} 100 | public static Singleton getInstance() { 101 | if (instance == null){ 102 | synchronized(Singleton.class){ 103 | if (instance == null) 104 | instance = new Singleton(); 105 | } 106 | } 107 | return instance; 108 | } 109 | } 110 | ``` 111 | **1.**第一个if判断,是为了降低锁的出现概率,前一段代码,只要执行到同一个方法都会触发锁,而这里只有`singleton`为空的时候才会触发,第一个进入的线程会创建对象,等其他线程再进入时对象已创建就不会继续创建,如果对整个方法同步,所有获取单例的线程都要排队,效率就会降低。 112 | **2.**第二个if判断是和之前的代码起一样的作用。 113 | 114 | 上面的代码看起来已经像是没有问题了,事实上,还有有很小的概率出现问题,那么我们先来了解:**原子操作**,**指令重排**。 115 | ### **1.原子操作** 116 | > * 原子操作,可以理解为不可分割的操作,就是它已经小到不可以再切分为多个操作进行,那么在计算机中要么它完全执行了,要么它完全没有执行,它不会存在执行到中间状态,可以理解为没有中间状态。比如:赋值语句就是一个原子操作: 117 | ```java 118 | n = 1; //这是一个原子操作 119 | ``` 120 | 假设n的值以前是0,那么这个操作的背后就是要么执行成功n等于1,要么没有执行成功n等于0,不会存在中间状态,就算是并发的过程中也是一样的。 121 | 下面看一句**不是原子操作**的代码: 122 | ```java 123 | int n =1; //不是原子操作 124 | ``` 125 | **原因:**这个语句中可以拆分为两个操作,1.声明变量n,2.给变量赋值为1,从中我们可以看出有一种状态是n被声明后但是没有来得及赋值的状态,这样的情况,在并发中,如果多个线程同时使用n,那么就会可能导致不稳定的结果。 126 | 127 | ### **2.指令重排** 128 | 所谓指令重排,就是计算机会对我们代码进行优化,优化的过程中会在不影响最后结果的前提下,调整原子操作的顺序。比如下面的代码: 129 | ```java 130 | int a ; // 语句1 131 | a = 1 ; // 语句2 132 | int b = 2 ; // 语句3 133 | int c = a + b ; // 语句4 134 | ``` 135 | 正常的情况,执行顺序应该是1234,但是实际有可能是3124,或者1324,这是因为语句3和4都没有原子性问题,那么就有可能被拆分成原子操作,然后重排. 136 | 原子操作以及指令重排的基本了解到这里结束,看回我们的代码: 137 | 138 | > 主要是`instance = new Singleton()`,根据我们所说的,这个语句不是原子操作,那么就会被拆分,事实上JVM(java虚拟机)对这个语句做的操作: 139 | - 1.给instance分配了内存 140 | - 2.调用Singleton的构造函数初始化了一个成员变量,产生了实例,放在另一处内存空间中 141 | - 3.将instance对象指向分配的内存空间,执行完这一步才算真的完成了,instance才不是null。 142 | 143 | 在一个线程里面是没有问题的,那么在多个线程中,JVM做了指令重排的优化就有可能导致问题,因为第二步和第三步的顺序是不能够保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 `instance` 已经是`非 null` 了(但却没有初始化),所以线程二会直接返回`instance`,然后使用,就会报空指针。 144 | 从更上一层来说,有一个线程是**instance已经不为null但是仍没有完成初始化**中间状态,这个时候有一个线程刚刚好执行到第一个if(`instance==null`),这里得到的`instance`已经不是`null`,然后他直接拿来用了,就会出现错误。 145 | 对于这个问题,我们使用的方案是加上`volatile`关键字。 146 | ```java 147 | public class Singleton { 148 | private static volatile Singleton instance = null; 149 | private Singleton() {} 150 | public static Singleton getInstance() { 151 | if (instance == null){ 152 | synchronized(Singleton.class){ 153 | if (instance == null) 154 | instance = new Singleton(); 155 | } 156 | } 157 | return instance; 158 | } 159 | } 160 | ``` 161 | `volatile`的作用:禁止指令重排,把`instance`声明为`volatile`之后,这样,在它的赋值完成之前,就不会调用读操作。也就是在一个线程没有彻底完成`instance = new Singleton()`;之前,其他线程不能够去调用读操作。 162 | 163 | > * 上面的方法实现单例都是基于没有**复杂序列化和反射**的时候,否则还是有可能有问题的,还有最后一种方法是使用枚举来实现单例,这个可以说的比较理想化的单例模式,自动支持序列化机制,绝对防止多次实例化。 164 | ```java 165 | public enum Singleton { 166 | INSTANCE; 167 | public void doSomething() { 168 | 169 | } 170 | } 171 | ``` 172 | 173 | 以上最推荐枚举方式,当然现在计算机的资源还是比较足够的,饿汉方式也是不错的,其中懒汉模式下,如果涉及多线程的问题,也需要注意写法。 174 | 175 | 最后提醒一下,`volatile`关键字,只禁止指令重排序,保证可见性(一个线程修改了变量,对任何其他线程来说都是立即可见的,因为会立即同步到主内存),但是不保证原子性。 -------------------------------------------------------------------------------- /设计模式/设计模式【2.1】--工厂方法模式.md: -------------------------------------------------------------------------------- 1 | 还记得前面的简单工厂模式么?我们开了一个水果工厂`FruitFactory`,生产三种水果`Apple`,`Pear`,`Orange`。今天给大家讲讲工厂方法模式: 2 | 3 | 老板搞水果工厂赚了点小钱,准备扩大生产,每个工厂生产一种水果,各司其职,而不是把所有的产品类型都放到一个工厂中。 4 | 5 | 既然有多工厂,那我们和之前一样,搞一个水果工厂类`FruitFactory`,把它搞成接口类。 6 | ```java 7 | import factory.Fruit; 8 | public interface FruitFactory { 9 | public Fruit getFruit(); 10 | } 11 | ``` 12 | 水果类的定义还是一样,定义一个水果接口`Fruit`: 13 | ```java 14 | public interface Fruit { 15 | public void process(); 16 | } 17 | ``` 18 | 19 | 水果分别如下,我直接写到一块去了: 20 | ```java 21 | public class Apple implements Fruit { 22 | public void process() { 23 | System.out.println("I am an Apple"); 24 | } 25 | } 26 | public class Pear implements Fruit { 27 | public void process() { 28 | System.out.println("I am a Pear"); 29 | } 30 | } 31 | public class Orange implements Fruit { 32 | public void process() { 33 | System.out.println("I am an Orange"); 34 | } 35 | } 36 | ``` 37 | 既然有多个工厂。那我们分别定义多个工厂,对水果工厂类做不同的实现,分别生产苹果,雪梨,橙子。 38 | ```java 39 | public class AppleFactory implements FruitFactory { 40 | public Fruit getFruit(){ 41 | return new Apple(); 42 | } 43 | } 44 | public class PearFactory implements FruitFactory { 45 | public Fruit getFruit(){ 46 | return new Pear(); 47 | } 48 | } 49 | public class OrangeFactory implements FruitFactory { 50 | public Fruit getFruit(){ 51 | return new Orange(); 52 | } 53 | } 54 | ``` 55 | 56 | 测试代码如下: 57 | ```java 58 | public class FruitTest { 59 | public static void main(String[] args) { 60 | FruitFactory appleFactory = new AppleFactory(); 61 | Fruit apple = appleFactory.getFruit(); 62 | apple.process(); 63 | 64 | FruitFactory pearFactory = new PearFactory(); 65 | Fruit pear = pearFactory.getFruit(); 66 | pear.process(); 67 | } 68 | } 69 | 70 | ``` 71 | 72 | 运行结果: 73 | ```cmd 74 | I am an Apple 75 | I am an Pear 76 | ``` 77 | 78 | 上面的写法,如果后续还有生产不同的水果,或者不同的水果工厂,相对容易拓展。总结一下,工厂方法模式一共有以下的角色: 79 | - 抽象工厂:提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 `getFruit()` 来创建水果产品。 80 | - 具体工厂:主要是实现抽象工厂中的抽象方法,创建具体的产品。 81 | - 抽象产品:定义了产品规范,比如所有的水果共同的特性。 82 | - 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。 83 | 84 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210111235819.png) 85 | 86 | 那么工厂方法模式,相对简单工厂模式,有什么区别呢? 87 | 工厂方法模式,一种工厂对应一种产品,各司其职,如果产品很多的话,方便分开维护,特别是那种创建产品比较复杂的场景,而产品类型又比较多,这样就会显得很臃肿。 88 | 89 | 但是如果产品很少,而且创建过程相对简单的时候,感觉简单工厂模式已经足够,不需要特意为了使用一种设计模式而使用它,过度设计会带了很多不必要的麻烦。 90 | 91 | 92 | -------------------------------------------------------------------------------- /设计模式/设计模式【2.2】--抽象工厂模式.md: -------------------------------------------------------------------------------- 1 | 还记得之前的工厂方法模式么?现在老板更加富有了,除了水果,还准备搞点其他的生意,再做点服装生意。水果工厂和服装工厂明显就不是同一种东西,肯定不能放到一个工厂里面生产,服装也有好几种,但是不同的工厂,也是老板自己的,老板希望能够把握大局,所以不同工厂,肯定是有同一个特征的,也就是他们都是工厂,并且都是老板的。 2 | 3 | 先来创建一个接口`Fruit.java`: 4 | ```java 5 | public interface Fruit { 6 | public void print(); 7 | } 8 | ``` 9 | 10 | 创建水果的实体类`Apple.java`: 11 | ```java 12 | public class Apple implements Fruit{ 13 | @Override 14 | public void print() { 15 | System.out.println("苹果产品生产出来了"); 16 | } 17 | } 18 | 19 | ``` 20 | 21 | `Pear.java`: 22 | ```java 23 | public class Pear implements Fruit{ 24 | @Override 25 | public void print() { 26 | System.out.println("雪梨产品生产出来了"); 27 | } 28 | } 29 | ``` 30 | 31 | 除了水果产品,我们还有服装产品,因此先创建一个`Clothes.java`: 32 | ```java 33 | public interface Clothes { 34 | public void process(); 35 | } 36 | ``` 37 | 服装厂有哪些产品呢?先生产两种`Jeans.java`: 38 | ```java 39 | public class Jeans implements Clothes{ 40 | @Override 41 | public void process() { 42 | System.out.println("牛仔裤生产出来了"); 43 | } 44 | } 45 | ``` 46 | 47 | `Dresses.java`: 48 | ```java 49 | public class Dresses implements Clothes{ 50 | @Override 51 | public void process() { 52 | System.out.println("连衣裙生产出来了"); 53 | } 54 | } 55 | ``` 56 | 57 | 既然也要生产水果产品,也要生产服装产品,那就先搞个抽闲工厂`AbstractFactory.java`,有两个方法,一个生产水果产品,一个生产服装产品: 58 | ```java 59 | public abstract class AbstractFactory { 60 | // 生产水果 61 | public abstract Fruit getFruit(String name); 62 | // 生产衣服 63 | public abstract Clothes getClothes(String name); 64 | } 65 | 66 | ``` 67 | 68 | 上面只是工厂的大致模型,但是每一种工厂都有自己的特点,所以水果工厂`FruitFactory.java`要自己实现生产水果产品的过程: 69 | ```java 70 | public class FruitFactory extends AbstractFactory{ 71 | 72 | @Override 73 | public Fruit getFruit(String name) { 74 | if(name==null){ 75 | return null; 76 | } 77 | if("Apple".equalsIgnoreCase(name)){ 78 | return new Apple(); 79 | }else if("Pear".equalsIgnoreCase(name)){ 80 | return new Pear(); 81 | } 82 | return null; 83 | } 84 | 85 | @Override 86 | public Clothes getClothes(String name) { 87 | return null; 88 | } 89 | } 90 | ``` 91 | 92 | 同样,衣服工厂`ClothesFactory.java`也要自己实现工厂的具体生产过程: 93 | ```java 94 | public class ClothesFactory extends AbstractFactory { 95 | @Override 96 | public Fruit getFruit(String name) { 97 | return null; 98 | } 99 | 100 | @Override 101 | public Clothes getClothes(String name) { 102 | if (name == null) { 103 | return null; 104 | } 105 | if ("Jeans".equalsIgnoreCase(name)) { 106 | return new Jeans(); 107 | } else if ("Dresses".equalsIgnoreCase(name)) { 108 | return new Dresses(); 109 | } 110 | return null; 111 | } 112 | } 113 | ``` 114 | 115 | 不同的工厂怎么管理呢?需要一个工厂创造器`FactoryProducer.java`,把不同的工厂造出来,也可以理解成为工厂的工厂: 116 | ```java 117 | public class FactoryProducer { 118 | public static AbstractFactory getFactory(String type) { 119 | if (type == null) { 120 | return null; 121 | } 122 | if ("Fruit".equalsIgnoreCase(type)) { 123 | return new FruitFactory(); 124 | } else if ("Clothes".equalsIgnoreCase(type)) { 125 | return new ClothesFactory(); 126 | } 127 | return null; 128 | } 129 | } 130 | ``` 131 | 132 | 测试代码`FactoryTest.java`: 133 | ```java 134 | public class FactoryTest { 135 | public static void main(String[] args) { 136 | AbstractFactory fruitFactory = FactoryProducer.getFactory("Fruit"); 137 | Fruit apple = fruitFactory.getFruit("apple"); 138 | apple.print(); 139 | 140 | Fruit pear = fruitFactory.getFruit("pear"); 141 | pear.print(); 142 | 143 | AbstractFactory clothesFactory = FactoryProducer.getFactory("Clothes"); 144 | Clothes jeans = clothesFactory.getClothes("jeans"); 145 | jeans.process(); 146 | 147 | Clothes dresses = clothesFactory.getClothes("Dresses"); 148 | dresses.process(); 149 | } 150 | } 151 | ``` 152 | 153 | 测试结果如下: 154 | ```java 155 | 苹果产品生产出来了 156 | 雪梨产品生产出来了 157 | 牛仔裤生产出来了 158 | 连衣裙生产出来了 159 | ``` 160 | 161 | 抽象工厂模式,本质上也是工厂模式,也是属于创建型模式,用于创建对象。其特点是用一个超级工厂创建其他的工厂,也就是工厂的工厂。每一个工厂管理一类的产品,比如水果工厂主要负责生产水果产品,服装工厂服装生产服装产品。而每一种工厂本质上都是工厂,都从抽象工厂中衍生出来,只是做了不同的实现。 162 | 163 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/20210123235026.png) 164 | 165 | - 优点:如果需要增加其他的工厂,容易拓展,增加工厂中的产品,也可以拓展。不需要关心创建的细节,相同的类型产品放置在一起创建。 166 | - 缺点:增加了抽象的复杂度,理解难度增加。 -------------------------------------------------------------------------------- /设计模式/设计模式【2】--简单工厂模式.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | ## 1.简单工厂模式介绍 4 | 5 | 工厂模式,比较常用,属于创建型模式,也就是主要是用来创建对象的。工厂模式,有三种,主要分为: 6 | 7 | - 简单工厂模式 8 | - 工厂方法模式 9 | - 抽象工厂模式 10 | 11 | 其中,本文要讲的就是,简单工厂模式,但是简单工厂模式,并不是属于`GoF`讲的23种设计模式中。简单工厂模式,也叫静态工厂方法模式。简单而言,就是有一个具体的工厂类,用来生产不同类型的对象,而这些对象,都有相似的特点,它们都实现同一个接口。 12 | 13 | 14 | 15 | 什么时候应该使用工厂模式?为什么需要工厂模式呢? 16 | 17 | 工厂模式主要是用来生成不同的对象,也就是屏蔽了对象生成的时候的复杂性,使用的时候不需要知道对象是怎么生成的,而只需要关注要生成什么对象。如果构造一个对象特别的费劲,而我们又经常需要构造生成这个对象,那么使用工厂模式是比较有利的。我们都知道,设计模式主要就是为了设计出更加简洁,易懂,方便维护,方便拓展的代码。 18 | 19 | 如果一个很复杂的对象,要在多个地方构建,那么要是改动一次,我们就需要找出所有引用的地方,逐一修改,那会很麻烦。 20 | 21 | 22 | 23 | 简单工厂模式主要有三种角色: 24 | 25 | - 简单工厂:负责创建所有的实例,依照不同的类型创建不同的对象,也就是产品。 26 | - 抽象产品接口:也就是所有产品的一个抽象,一般是所有产品都需要实现的接口。 27 | - 具体产品:实现抽象产品接口,不同的产品做不一样的实现。 28 | 29 | ## 2.简单工厂模式举例 30 | 31 | 假设现在有一个果园,用来种植各种水果,但是每一种水果种植的方式又不一样。首先,先定义一个接口`Fruit`: 32 | 33 | ```java 34 | public interface Fruit { 35 | public void process(); 36 | } 37 | ``` 38 | 39 | 定义三种水果`Apple`,`Pear`,`Orange`: 40 | 41 | ```java 42 | public class Apple implements Fruit{ 43 | public void process() { 44 | System.out.println("I am an Apple"); 45 | } 46 | } 47 | ``` 48 | 49 | ```java 50 | public class Pear implements Fruit{ 51 | public void process() { 52 | System.out.println("I am a Pear"); 53 | } 54 | } 55 | ``` 56 | 57 | ```java 58 | public class Orange implements Fruit{ 59 | public void process() { 60 | System.out.println("I am an Orange"); 61 | } 62 | } 63 | ``` 64 | 65 | 创建一个工厂类: 66 | 67 | ```java 68 | public class FruitFactory { 69 | public static Fruit getFruit(String name) { 70 | if ("Apple".equalsIgnoreCase(name)) { 71 | return new Apple(); 72 | } else if ("Pear".equalsIgnoreCase(name)) { 73 | return new Pear(); 74 | } else if ("Orange".equalsIgnoreCase(name)) { 75 | return new Orange(); 76 | } 77 | return null; 78 | } 79 | } 80 | ``` 81 | 82 | 测试代码如下: 83 | 84 | ```java 85 | public class FruitTest { 86 | public static void main(String[] args) { 87 | Fruit apple = FruitFactory.getFruit("Apple"); 88 | apple.process(); 89 | Fruit pear = FruitFactory.getFruit("Pear"); 90 | pear.process(); 91 | Fruit orange = FruitFactory.getFruit("Orange"); 92 | orange.process(); 93 | } 94 | } 95 | ``` 96 | 97 | 测试结果如下: 98 | 99 | image-20201218231609645 100 | 101 | 这样的写法,如果后续再来了一种水果,那么只需要实现接口,同时在工厂中增加一个`case`即可。 102 | 103 | 104 | 105 | ## 3.简单工厂模式的优劣 106 | 107 | 优点: 108 | 109 | - 产品和工厂的职责比较分明,工厂负责创建,产品负责自己的实现 110 | - 产生/构建产品比较简单,不需要关注内部细节,只需要知道自己想要哪一种。 111 | - 增加或者修改产品比较简单,解耦合。 112 | 113 | 114 | 115 | 凡事都有优劣,简单工厂方法的缺点在于: 116 | 117 | - 工厂类的重要性很高,一旦出现问题,影响所有的产品。 118 | - 产品数量一旦特别多的时候,工厂内部逻辑会比较复杂,不利于理解和维护。 119 | - 静态方法不利于继承和实现。 120 | 121 | 122 | 123 | 从以上的优劣,我们可以知道,其实如果产品创建过程比较复杂,而且个数不多,都是依靠某些参数来创建的话,抽象出简单工厂模式,其实是比较有利的一种做法。 -------------------------------------------------------------------------------- /设计模式/设计模式【3.3】--CGLib源码分析.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 图片名称 4 | 5 | ## cglib 动态代理 6 | 7 | ### cglib介绍 8 | CGLIB 是一个开源项目,一个强大高性能高质量的代码生成库,可以在运行期拓展 Java 类,实现 Java 接口等等。底层是使用一个小而快的字节码处理框架 ASM,从而转换字节码和生成新的类。 9 | 10 | 理论上我们也可以直接用 ASM 来直接生成代码,但是要求我们对 JVM 内部,class 文件格式,以及字节码的指令集都很熟悉。 11 | 12 | **这玩意不在 JDK 的包里面,需要自己下载导入或者 Maven 坐标导入。** 13 | 14 | 我选择 `Maven` 导入, 加到 `pom.xml` 文件: 15 | ```xml 16 | 17 | 18 | cglib 19 | cglib 20 | 3.3.0 21 | 22 | 23 | 24 | ``` 25 | 26 | `Student.java`: 27 | ```java 28 | public class Student { 29 | public void learn() { 30 | System.out.println("我是学生,我想学习"); 31 | } 32 | } 33 | ``` 34 | 35 | `MyProxy.java`(代理类) 36 | ```java 37 | 38 | import net.sf.cglib.proxy.MethodInterceptor; 39 | import net.sf.cglib.proxy.MethodProxy; 40 | 41 | import java.lang.reflect.Method; 42 | 43 | public class StudentProxy implements MethodInterceptor { 44 | public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { 45 | // TODO Auto-generated method stub 46 | System.out.println("代理前 -------"); 47 | proxy.invokeSuper(obj, args); 48 | System.out.println("代理后 -------"); 49 | return null; 50 | } 51 | 52 | } 53 | 54 | ``` 55 | 测试类(`Test.java`) 56 | ```java 57 | 58 | import net.sf.cglib.core.DebuggingClassWriter; 59 | import net.sf.cglib.proxy.Enhancer; 60 | 61 | public class Test { 62 | public static void main(String[] args) { 63 | // 代理类class文件存入本地磁盘方便我们反编译查看源码 64 | System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/aphysia/Desktop"); 65 | 66 | Enhancer enhancer = new Enhancer(); 67 | enhancer.setSuperclass(Student.class); 68 | enhancer.setCallback(new StudentProxy()); 69 | Student student = (Student) enhancer.create(); 70 | student.learn(); 71 | 72 | } 73 | } 74 | 75 | ``` 76 | 77 | 运行之后的结果是: 78 | ```shell 79 | CGLIB debugging enabled, writing to '/Users/xuwenhao/Desktop' 80 | 代理前 ------- 81 | 我是学生,我想学习 82 | 代理后 ------- 83 | ``` 84 | 85 | 在我们选择的文件夹里面,生成了代理类的代码: 86 | 87 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210925172613.png) 88 | 89 | ### 源码分析 90 | 91 | 我们先要代理的类,需要实现`MethodInterceptor`(方法拦截器)接口,这个接口只有一个方法 `intercept`,参数分别是: 92 | - obj:需要增强的对象 93 | - method:需要拦截的方法 94 | - args:要被拦截的方法参数 95 | - proxy:表示要触发父类的方法对象 96 | 97 | ```java 98 | package net.sf.cglib.proxy; 99 | import java.lang.reflect.Method; 100 | public interface MethodInterceptor extends Callback { 101 | public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, 102 | MethodProxy proxy) throws Throwable; 103 | } 104 | ``` 105 | 106 | 再看回我们要创建代理类的方法 `enhancer.create()`,这个方法的意思:如果需要,生成一个新类,并使用指定的回调(如果有的话)来创建一个新的对象实例。使用超类的无参数构造函数。 107 | 108 | ```java 109 | public Object create() { 110 | classOnly = false; 111 | argumentTypes = null; 112 | return createHelper(); 113 | } 114 | ``` 115 | 116 | 主要的方法逻辑我们得看 `createHelper()`,除了校验,就是调用 `KEY_FACTORY.newInstance()` 方法生成 `EnhancerKey`对象,`KEY_FACTORY` 是静态 `EnhancerKey` 接口,`newInstance()`是接口里面的一个方法,重点在`super.create(key)`里面,调用的是父类的方法: 117 | 118 | ```java 119 | private Object createHelper() { 120 | // 校验 121 | preValidate(); 122 | Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null, 123 | ReflectUtils.getNames(interfaces), 124 | filter == ALL_ZERO ? null : new WeakCacheKey(filter), 125 | callbackTypes, 126 | useFactory, 127 | interceptDuringConstruction, 128 | serialVersionUID); 129 | this.currentKey = key; 130 | Object result = super.create(key); 131 | return result; 132 | } 133 | ``` 134 | 135 | `AbstractClassGenerator` 是 `Enhancer` 的父类,`create(key)` 方法的主要逻辑是获取类加载器,缓存获取类加载数据,然后再反射构造对象,里面有两个创造实例对象的方法: 136 | - `fistInstance()`: 不应该在常规流中调用此方法。从技术上讲,`{@link #wrapCachedClass(Class)}`使用`{@link EnhancerFactoryData}`作为缓存值,后者支持比普通的旧反射查找和调用更快的实例化。出于向后兼容性的原因,这个方法保持不变:只是以防它曾经被使用过。(我的理解是目前的逻辑不会走到这个分支,因为它比较忙,但是为了兼容,这个case还保存着),内部逻辑其实用的是`ReflectUtils.newInstance(type)`。 137 | - `nextInstance()`: 真正的创建代理对象的类 138 | 139 | ```java 140 | protected Object create(Object key) { 141 | try { 142 | ClassLoader loader = getClassLoader(); 143 | Map cache = CACHE; 144 | ClassLoaderData data = cache.get(loader); 145 | if (data == null) { 146 | synchronized (AbstractClassGenerator.class) { 147 | cache = CACHE; 148 | data = cache.get(loader); 149 | if (data == null) { 150 | Map newCache = new WeakHashMap(cache); 151 | data = new ClassLoaderData(loader); 152 | newCache.put(loader, data); 153 | CACHE = newCache; 154 | } 155 | } 156 | } 157 | this.key = key; 158 | Object obj = data.get(this, getUseCache()); 159 | if (obj instanceof Class) { 160 | return firstInstance((Class) obj); 161 | } 162 | // 真正创建对象的方法 163 | return nextInstance(obj); 164 | } catch (RuntimeException e) { 165 | throw e; 166 | } catch (Error e) { 167 | throw e; 168 | } catch (Exception e) { 169 | throw new CodeGenerationException(e); 170 | } 171 | } 172 | ``` 173 | 174 | 这个方法定义在`AbstractClassGenerator`,但是实际上是调用子类 `Enhancer`的实现,主要是通过获取参数类型,参数,以及回调对象,用这些参数反射生成代理对象。 175 | 176 | ```java 177 | protected Object nextInstance(Object instance) { 178 | EnhancerFactoryData data = (EnhancerFactoryData) instance; 179 | 180 | if (classOnly) { 181 | return data.generatedClass; 182 | } 183 | 184 | Class[] argumentTypes = this.argumentTypes; 185 | Object[] arguments = this.arguments; 186 | if (argumentTypes == null) { 187 | argumentTypes = Constants.EMPTY_CLASS_ARRAY; 188 | arguments = null; 189 | } 190 | // 构造 191 | return data.newInstance(argumentTypes, arguments, callbacks); 192 | } 193 | ``` 194 | 195 | 内部实现逻辑,调用的都是`ReflectUtils.newInstance()`, 参数种类不一样: 196 | ```java 197 | public Object newInstance(Class[] argumentTypes, Object[] arguments, Callback[] callbacks) { 198 | setThreadCallbacks(callbacks); 199 | try { 200 | 201 | if (primaryConstructorArgTypes == argumentTypes || 202 | Arrays.equals(primaryConstructorArgTypes, argumentTypes)) { 203 | return ReflectUtils.newInstance(primaryConstructor, arguments); 204 | } 205 | return ReflectUtils.newInstance(generatedClass, argumentTypes, arguments); 206 | } finally { 207 | 208 | setThreadCallbacks(null); 209 | } 210 | 211 | } 212 | ``` 213 | 214 | 跟进去到底,就是获取构造器方法,反射方式构造代理对象,最终调用到的是 JDK 提供的方法: 215 | 216 | ```java 217 | public static Object newInstance(Class type, Class[] parameterTypes, Object[] args) { 218 | return newInstance(getConstructor(type, parameterTypes), args); 219 | } 220 | public static Object newInstance(final Constructor cstruct, final Object[] args) { 221 | 222 | boolean flag = cstruct.isAccessible(); 223 | try { 224 | if (!flag) { 225 | cstruct.setAccessible(true); 226 | } 227 | Object result = cstruct.newInstance(args); 228 | return result; 229 | } catch (InstantiationException e) { 230 | throw new CodeGenerationException(e); 231 | } catch (IllegalAccessException e) { 232 | throw new CodeGenerationException(e); 233 | } catch (InvocationTargetException e) { 234 | throw new CodeGenerationException(e.getTargetException()); 235 | } finally { 236 | if (!flag) { 237 | cstruct.setAccessible(flag); 238 | } 239 | } 240 | 241 | } 242 | ``` 243 | 244 | 打开它自动生成的代理类文件看看,就会发现其实也是生成那些方法,加上了一些增强方法: 245 | 246 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210925221249.png) 247 | 248 | 生成的代理类继承了原来的类: 249 | ```java 250 | public class Student$$EnhancerByCGLIB$$929cb5fe extends Student implements Factory { 251 | ... 252 | } 253 | ``` 254 | 255 | 看看生成的增强方法,其实是调用到 `intercept()`方法,这个方法由我们前面自己实现,因此就完成了代理对象增强的功能: 256 | ```java 257 | public final void learn() { 258 | MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; 259 | if (var10000 == null) { 260 | CGLIB$BIND_CALLBACKS(this); 261 | var10000 = this.CGLIB$CALLBACK_0; 262 | } 263 | 264 | if (var10000 != null) { 265 | var10000.intercept(this, CGLIB$learn$0$Method, CGLIB$emptyArgs, CGLIB$learn$0$Proxy); 266 | } else { 267 | super.learn(); 268 | } 269 | } 270 | ``` 271 | 272 | ## cglib 和 jdk 动态代理有什么区别 273 | 274 | 1. jdk 动态代理是利用拦截器加上反射生成了一个代理接口的匿名类,执行方法的时候交给 InvokeHandler 处理。CGLIB 动态代理是使用了 ASM框架,修改原来的字节码,然后生成新的子类来处理。 275 | 2. JDK 代理需要实现接口,但是CGLIB不强制。 276 | 3. 在JDK1.6之前,cglib因为用了字节码生成技术,比反射效率高,但是之后jdk也进行了一些优化,效率上已经提升了。 277 | 278 | 279 | -------------------------------------------------------------------------------- /设计模式/设计模式【4】--建造者模式.md: -------------------------------------------------------------------------------- 1 | 开局一张图,剩下全靠写... 2 | 3 | 4 | 5 | ## 引言 6 | 7 | 如果你用过 `Mybatis` ,相信你对以下代码的写法并不陌生,先创建一个`builder`对象,然后再调用`.build()`函数: 8 | 9 | ```java 10 | InputStream is = Resources.getResourceAsStream("mybatis.xml"); 11 | SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); 12 | SqlSession sqlSession = sqlSessionFactory.openSession(); 13 | ``` 14 | 15 | 上面其实就是我们这篇文章所要讲解的 **建造者模式**,下面让我们一起来琢磨一下它。 16 | 17 | 18 | 19 | ## 什么是建造者模式 20 | 21 | > 建造者模式是设计模式的一种,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。(来源于百度百科) 22 | 23 | 建造者模式,其实是创建型模式的一种,也是23种设计模式中的一种,从上面的定义来看比较模糊,但是不得不承认,当我们有能力用简洁的话去定义一个东西的时候,我们才是真的了解它了,因为这个时候我们已经知道它的界限在哪。 24 | 25 | 所谓将一个复杂对象的构建与它的表示分离,就是将对象的构建器抽象出来,构造的过程一样,但是不一样的构造器可以实现不一样的表示。 26 | 27 | 28 | 29 | ## 结构与例子 30 | 31 | 建造者模式主要分为以下四种角色: 32 | 33 | - 产品(`Product`):具体生产器要构造的复杂对象 34 | - 抽象生成器(`Bulider`):抽象生成器是一个接口,创建一个产品各个部件的接口方法,以及返回产品的方法 35 | - 具体建造者(`ConcreteBuilder`):按照自己的产品特性,实现抽象建造者对应的接口 36 | - 指挥者(`Director`):创建一个复杂的对象,控制具体的流程 37 | 38 | 39 | 40 | 说到这里,可能会有点懵,毕竟全都是定义,下面从实际例子来讲讲,就拿程序员最喜欢的电脑来说,假设现在要生产多种电脑,电脑有屏幕,鼠标,cpu,主板,磁盘,内存等等,我们可能立马就能写出来: 41 | 42 | ```java 43 | public class Computer { 44 | private String screen; 45 | private String mouse; 46 | private String cpu; 47 | private String mainBoard; 48 | private String disk; 49 | private String memory; 50 | ... 51 | public String getMouse() { 52 | return mouse; 53 | } 54 | 55 | public void setMouse(String mouse) { 56 | this.mouse = mouse; 57 | } 58 | 59 | public String getCpu() { 60 | return cpu; 61 | } 62 | 63 | public void setCpu(String cpu) { 64 | this.cpu = cpu; 65 | } 66 | ... 67 | } 68 | ``` 69 | 70 | 上面的例子中,每一种属性都使用单独的`set`方法,要是生产不同的电脑的不同部件,具体的实现还不太一样,这样一个类实现起来貌似不是很优雅,比如联想电脑和华硕电脑的屏幕的构建过程不一样,而且这些部件的构建,理论上都是电脑的一部分,我们可以考虑**流水线式**的处理。 71 | 72 | 73 | 74 | 当然,也有另外一种实现,就是多个构造函数,不同的构造函数带有不同的参数,实现了可选的参数: 75 | 76 | ```java 77 | public class Computer { 78 | private String screen; 79 | private String mouse; 80 | private String cpu; 81 | private String mainBoard; 82 | private String disk; 83 | private String memory; 84 | 85 | public Computer(String screen) { 86 | this.screen = screen; 87 | } 88 | 89 | public Computer(String screen, String mouse) { 90 | this.screen = screen; 91 | this.mouse = mouse; 92 | } 93 | 94 | public Computer(String screen, String mouse, String cpu) { 95 | this.screen = screen; 96 | this.mouse = mouse; 97 | this.cpu = cpu; 98 | } 99 | ... 100 | } 101 | ``` 102 | 103 | 上面多种参数的构造方法,理论上满足了按需构造的要求,但是还是会有不足的地方: 104 | 105 | - 倘若构造每一个部件的过程都比较复杂,那么构造函数看起来就比较凌乱 106 | - 如果有多种按需构造的要求,构造函数就太多了 107 | - 构造不同的电脑类型,耦合在一块,必须抽象出来 108 | 109 | 110 | 111 | 首先,我们先用流水线的方式,实现按需构造,不能重载那么多构造函数: 112 | 113 | ```java 114 | public class Computer { 115 | private String screen; 116 | private String mouse; 117 | private String cpu; 118 | private String mainBoard; 119 | private String disk; 120 | private String memory; 121 | 122 | public Computer setScreen(String screen) { 123 | this.screen = screen; 124 | return this; 125 | } 126 | 127 | public Computer setMouse(String mouse) { 128 | this.mouse = mouse; 129 | return this; 130 | } 131 | 132 | public Computer setCpu(String cpu) { 133 | this.cpu = cpu; 134 | return this; 135 | } 136 | 137 | public Computer setMainBoard(String mainBoard) { 138 | this.mainBoard = mainBoard; 139 | return this; 140 | } 141 | 142 | public Computer setDisk(String disk) { 143 | this.disk = disk; 144 | return this; 145 | } 146 | 147 | public Computer setMemory(String memory) { 148 | this.memory = memory; 149 | return this; 150 | } 151 | } 152 | ``` 153 | 154 | 使用的时候,构造起来,就像是流水线一样,一步一步构造就可以: 155 | 156 | ```java 157 | Computer computer = new Computer() 158 | .setScreen("高清屏幕") 159 | .setMouse("罗技鼠标") 160 | .setCpu("i7处理器") 161 | .setMainBoard("联想主板") 162 | .setMemory("32G内存") 163 | .setDisk("512G磁盘"); 164 | ``` 165 | 166 | 但是以上的写法不够优雅,既然构造过程可能很复杂,为何不用一个特定的类来构造呢?这样构造的过程和主类就分离了,职责更加清晰,在这里内部类就可以了: 167 | 168 | ```java 169 | package designpattern.builder; 170 | 171 | import javax.swing.*; 172 | 173 | public class Computer { 174 | private String screen; 175 | private String mouse; 176 | private String cpu; 177 | private String mainBoard; 178 | private String disk; 179 | private String memory; 180 | 181 | Computer(Builder builder) { 182 | this.screen = builder.screen; 183 | this.cpu = builder.cpu; 184 | this.disk = builder.disk; 185 | this.mainBoard = builder.mainBoard; 186 | this.memory = builder.memory; 187 | this.mouse = builder.mouse; 188 | } 189 | 190 | public static class Builder { 191 | private String screen; 192 | private String mouse; 193 | private String cpu; 194 | private String mainBoard; 195 | private String disk; 196 | private String memory; 197 | 198 | public Builder setScreen(String screen) { 199 | this.screen = screen; 200 | return this; 201 | } 202 | 203 | public Builder setMouse(String mouse) { 204 | this.mouse = mouse; 205 | return this; 206 | } 207 | 208 | public Builder setCpu(String cpu) { 209 | this.cpu = cpu; 210 | return this; 211 | } 212 | 213 | public Builder setMainBoard(String mainBoard) { 214 | this.mainBoard = mainBoard; 215 | return this; 216 | } 217 | 218 | public Builder setDisk(String disk) { 219 | this.disk = disk; 220 | return this; 221 | } 222 | 223 | public Builder setMemory(String memory) { 224 | this.memory = memory; 225 | return this; 226 | } 227 | 228 | public Computer build() { 229 | return new Computer(this); 230 | } 231 | } 232 | } 233 | 234 | ``` 235 | 236 | 使用的时候,使用`builder`来构建,构建完成之后,调用build的时候,再将具体的值,赋予我们需要的对象(这里是`Computer`): 237 | 238 | ```java 239 | public class Test { 240 | public static void main(String[] args) { 241 | Computer computer = new Computer.Builder() 242 | .setScreen("高清屏幕") 243 | .setMouse("罗技鼠标") 244 | .setCpu("i7处理器") 245 | .setMainBoard("联想主板") 246 | .setMemory("32G内存") 247 | .setDisk("512G磁盘") 248 | .build(); 249 | System.out.println(computer.toString()); 250 | } 251 | } 252 | ``` 253 | 254 | 但是上面的写法,如果我们构造多种电脑,每种电脑的配置不太一样,构建的过程也不一样,那么我们就必须将构造器抽象出来,变成一个抽象类。 255 | 256 | 首先我们定义产品类`Computer`: 257 | 258 | ```java 259 | public class Computer { 260 | private String screen; 261 | private String mouse; 262 | private String cpu; 263 | 264 | public void setScreen(String screen) { 265 | this.screen = screen; 266 | } 267 | 268 | public void setMouse(String mouse) { 269 | this.mouse = mouse; 270 | } 271 | 272 | public void setCpu(String cpu) { 273 | this.cpu = cpu; 274 | } 275 | 276 | @Override 277 | public String toString() { 278 | return "Computer{" + 279 | "screen='" + screen + '\'' + 280 | ", mouse='" + mouse + '\'' + 281 | ", cpu='" + cpu + '\'' + 282 | '}'; 283 | } 284 | } 285 | ``` 286 | 287 | 定义一个抽象的构造类,用于所有的电脑类构造: 288 | 289 | ```java 290 | public abstract class Builder { 291 | abstract Builder buildScreen(String screen); 292 | abstract Builder buildMouse(String mouse); 293 | abstract Builder buildCpu(String cpu); 294 | 295 | abstract Computer build(); 296 | } 297 | ``` 298 | 299 | 先构造一台联想电脑,那联想电脑必须实现自己的构造器,每一款电脑总有自己特殊的地方: 300 | 301 | ```java 302 | public class LenovoBuilder extends Builder { 303 | private Computer computer = new Computer(); 304 | 305 | @Override 306 | Builder buildScreen(String screen) { 307 | computer.setScreen(screen); 308 | return this; 309 | } 310 | 311 | @Override 312 | Builder buildMouse(String mouse) { 313 | computer.setMouse(mouse); 314 | return this; 315 | } 316 | 317 | @Override 318 | Builder buildCpu(String cpu) { 319 | computer.setCpu(cpu); 320 | return this; 321 | } 322 | 323 | @Override 324 | Computer build() { 325 | System.out.println("构建中..."); 326 | return computer; 327 | } 328 | } 329 | 330 | ``` 331 | 332 | 333 | 334 | 构建器有了,还需要有个指挥者,它负责去构建我们具体的电脑: 335 | 336 | ```java 337 | public class Director { 338 | Builder builder = null; 339 | public Director(Builder builder){ 340 | this.builder = builder; 341 | } 342 | 343 | public void doProcess(String screen,String mouse,String cpu){ 344 | builder.buildScreen(screen) 345 | .buildMouse(mouse) 346 | .buildCpu(cpu); 347 | } 348 | } 349 | ``` 350 | 351 | 使用的时候,我们只需要先构建`builder`,然后把`builder`传递给指挥者,他负责具体的构建,构建完之后,构建器调用一下`.build()`方法,就可以创建出一台电脑。 352 | 353 | ```java 354 | public class Test { 355 | public static void main(String[] args) { 356 | LenovoBuilder builder = new LenovoBuilder(); 357 | Director director = new Director(builder); 358 | director.doProcess("联想屏幕","游戏鼠标","高性能cpu"); 359 | Computer computer = builder.build(); 360 | System.out.println(computer); 361 | } 362 | } 363 | ``` 364 | 365 | 打印结果: 366 | 367 | ```shell 368 | 构建中... 369 | Computer{screen='联想屏幕', mouse='游戏鼠标', cpu='高性能cpu'} 370 | ``` 371 | 372 | 以上其实就是完整的建造者模式,但是我们平时用的,大部分都是自己直接调用构建器`Builder`,一路`set()`,最后`build()`,就创建出了一个对象。 373 | 374 | ## 使用场景 375 | 376 | 构建这模式的好处是什么?首先想到的应该是将构建的过程解耦了,构建的过程如果很复杂,单独拎出来写,清晰简洁。其次,每个部分的构建,其实都是可以独立去创建的,不需要多个构造方法,构建的工作交给了构建器,而不是对象本身。专业的人做专业的事。同样,构建者模式也比较适用于不同的构造方法或者构造顺序,可能会产生不同的构造结果的场景。 377 | 378 | 但是缺点还是有的,需要维护多出来的`Builder`对象,如果多种产品之间的共性不多,那么抽象的构建器将会失去它该有的作用。如果产品类型很多,那么定义太多的构建类来实现这种变化,代码也会变得比较复杂。 379 | 380 | 最近在公司用`GRPC`,里面的对象几乎都是基于构建者模式,链式的构建确实写着很舒服,也比较优雅,代码是写给人看的,我们所做的一切设计模式,都是为了拓展,解耦,以及避免代码只能口口相传。 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | -------------------------------------------------------------------------------- /设计模式/设计模式【5】--原型模式.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 开局一张图,剩下全靠写... 4 | 5 | 6 | 7 | ## 前言 8 | 9 | 接触过 `Spring` 或者 `Springboot` 的同学或许都了解, `Bean` 默认是单例的,也就是全局共用同一个对象,不会因为请求不同,使用不同的对象,这里我们不会讨论单例,前面已经讨论过单例模式的好处以及各种实现,有兴趣可以了解一下:http://aphysia.cn/archives/designpattern1。除了单例以外,`Spring`还可以设置其他的作用域,也就是`scope="prototype"`,这就是原型模式,每次来一个请求,都会新创建一个对象,这个对象就是按照原型实例创建的。 10 | 11 | 12 | 13 | ## 原型模式的定义 14 | 15 | 原型模式,也是创建型模式的一种,是指用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,简单来说,就是拷贝。一般适用于: 16 | 17 | - 实例比较复杂,完全创建成本高,直接复制比较简单 18 | - 构造函数比较复杂,创建可能产生很多不必要的对象 19 | 20 | 优点: 21 | 22 | - 隐藏了创建实例的具体细节 23 | - 创建对象效率比较高 24 | - 如果一个对象大量相同的属性,只有少量需要特殊化的时候,可以直接用原型模式拷贝的对象,加以修改,就可以达到目的。 25 | 26 | ## 原型模式的实现方式 27 | 28 | 一般来说,原型模式就是用来复制对象的,那么复制对象必须有原型类,也就是`Prototype`,`Prototype`需要实现`Cloneable`接口,实现这个接口才能被拷贝,再重写`clone()`方法,还可以根据不同的类型来快速获取原型对象。 29 | 30 | 我们先定义一个原型类`Fruit`: 31 | 32 | ```java 33 | public abstract class Fruit implements Cloneable{ 34 | String name; 35 | float price; 36 | 37 | public String getName() { 38 | return name; 39 | } 40 | 41 | public void setName(String name) { 42 | this.name = name; 43 | } 44 | 45 | public float getPrice() { 46 | return price; 47 | } 48 | 49 | public void setPrice(float price) { 50 | this.price = price; 51 | } 52 | 53 | public Object clone() { 54 | Object clone = null; 55 | try { 56 | clone = super.clone(); 57 | } catch (CloneNotSupportedException e) { 58 | e.printStackTrace(); 59 | } 60 | return clone; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return "Fruit{" + 66 | "name='" + name + '\'' + 67 | ", price=" + price + 68 | '}'; 69 | } 70 | } 71 | ``` 72 | 73 | 以及拓展了`Fruit`类的实体类`Apple`,`Pear`,`Watermelon`: 74 | 75 | ```java 76 | public class Apple extends Fruit{ 77 | public Apple(float price){ 78 | name = "苹果"; 79 | this.price = price; 80 | } 81 | } 82 | ``` 83 | 84 | ```java 85 | public class Pear extends Fruit{ 86 | public Pear(float price){ 87 | name = "雪梨"; 88 | this.price = price; 89 | } 90 | } 91 | ``` 92 | 93 | ```java 94 | public class Watermelon extends Fruit{ 95 | public Watermelon(float price){ 96 | name = "西瓜"; 97 | this.price = price; 98 | } 99 | } 100 | ``` 101 | 102 | 创建一个获取不同水果类的缓存类,每次取的时候,根据不同的类型,取出来,拷贝一次返回即可: 103 | 104 | ```java 105 | public class FruitCache { 106 | private static ConcurrentHashMap fruitMap = 107 | new ConcurrentHashMap(); 108 | static { 109 | Apple apple = new Apple(10); 110 | fruitMap.put(apple.getName(),apple); 111 | 112 | Pear pear = new Pear(8); 113 | fruitMap.put(pear.getName(),pear); 114 | 115 | Watermelon watermelon = new Watermelon(5); 116 | fruitMap.put(watermelon.getName(),watermelon); 117 | } 118 | 119 | public static Fruit getFruit(String name){ 120 | Fruit fruit = fruitMap.get(name); 121 | return (Fruit)fruit.clone(); 122 | } 123 | } 124 | ``` 125 | 126 | 测试一下,分别获取不同的水果,以及对比两次获取同一种类型,可以发现,两次获取的同一种类型,不是同一个对象: 127 | 128 | ```java 129 | public class Test { 130 | public static void main(String[] args) { 131 | Fruit apple = FruitCache.getFruit("苹果"); 132 | System.out.println(apple); 133 | 134 | Fruit pear = FruitCache.getFruit("雪梨"); 135 | System.out.println(pear); 136 | 137 | Fruit watermelon = FruitCache.getFruit("西瓜"); 138 | System.out.println(watermelon); 139 | 140 | Fruit apple1 = FruitCache.getFruit("苹果"); 141 | System.out.println("是否为同一个对象" + apple.equals(apple1)); 142 | } 143 | } 144 | ``` 145 | 146 | 结果如下: 147 | 148 | ```txt 149 | 150 | Fruit{name='苹果', price=10.0} 151 | Fruit{name='雪梨', price=8.0} 152 | Fruit{name='西瓜', price=5.0} 153 | false 154 | ``` 155 | 156 | 再测试一下,我们看看里面的`name`属性是不是同一个对象: 157 | 158 | ```java 159 | public class Test { 160 | public static void main(String[] args) { 161 | Fruit apple = FruitCache.getFruit("苹果"); 162 | System.out.println(apple); 163 | 164 | Fruit apple1 = FruitCache.getFruit("苹果"); 165 | System.out.println(apple1); 166 | System.out.println("是否为同一个对象:" + apple.equals(apple1)); 167 | System.out.println("是否为同一个字符串对象:" + apple.name.equals(apple1.name)); 168 | } 169 | } 170 | ``` 171 | 172 | 结果如下,里面的字符串确实还是用的是同一个对象: 173 | 174 | ```txt 175 | Fruit{name='苹果', price=10.0} 176 | Fruit{name='苹果', price=10.0} 177 | 是否为同一个对象:false 178 | 是否为同一个字符串对象:true 179 | ``` 180 | 181 | 这是为什么呢?**因为上面使用的clone()是浅拷贝!!!不过有一点,字符串在Java里面是不可变的,如果发生修改,也不会修改原来的字符串,由于这个属性的存在,类似于深拷贝**。如果属性是其他自定义对象,那就得注意了,浅拷贝不会真的拷贝该对象,只会拷贝一份引用。 182 | 183 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211117233446.png) 184 | 185 | 这里不得不介绍一下浅拷贝与深拷贝的区别: 186 | 187 | - 浅拷贝:没有真正的拷贝数据,只是拷贝了一个指向数据内存地址的指针 188 | - 深拷贝:不仅新建了指针,还拷贝了一份数据内存 189 | 190 | 191 | 192 | 如果我们使用`Fruit apple = apple1`,这样只是拷贝了对象的引用,其实本质上还是同一个对象,上面的情况虽然对象是不同的,但是`Apple`属性的拷贝还属于同一个引用,地址还是一样的,它们共享了原来的属性对象`name`。 193 | 194 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211117233702.png) 195 | 196 | **那如何进行深拷贝呢?**一般有以下方案: 197 | 198 | - 直接 new 对象,这个不用考虑了 199 | 200 | - 序列化与反序列化:先序列化之后,再反序列化回来,就可以得到一个新的对象,注意必须实现`Serializable`接口。 201 | - 自己重写对象的`clone()`方法 202 | 203 | 204 | 205 | ### 序列化实现深拷贝 206 | 207 | 序列化实现代码如下: 208 | 209 | 创建一个`Student`类和`School`类: 210 | 211 | ```java 212 | import java.io.Serializable; 213 | 214 | public class Student implements Serializable { 215 | String name; 216 | 217 | School school; 218 | 219 | public Student(String name, School school) { 220 | this.name = name; 221 | this.school = school; 222 | } 223 | } 224 | ``` 225 | 226 | ```java 227 | import java.io.Serializable; 228 | 229 | public class School implements Serializable { 230 | String name; 231 | 232 | public School(String name) { 233 | this.name = name; 234 | } 235 | } 236 | ``` 237 | 238 | 序列化拷贝的类: 239 | 240 | ```java 241 | import java.io.ByteArrayInputStream; 242 | import java.io.ByteArrayOutputStream; 243 | import java.io.ObjectInputStream; 244 | import java.io.ObjectOutputStream; 245 | import java.io.Serializable; 246 | 247 | public class CloneUtil { 248 | public static T clone(T obj) { 249 | T result = null; 250 | try { 251 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 252 | ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); 253 | objectOutputStream.writeObject(obj); 254 | objectOutputStream.close(); 255 | 256 | ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); 257 | ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); 258 | // 返回生成的新对象 259 | result = (T) objectInputStream.readObject(); 260 | objectInputStream.close(); 261 | } catch (Exception e) { 262 | e.printStackTrace(); 263 | } 264 | return result; 265 | } 266 | } 267 | ``` 268 | 269 | 测试类: 270 | 271 | ```java 272 | 273 | public class Test { 274 | public static void main(String[] args) { 275 | School school = new School("东方小学"); 276 | Student student =new Student("小明",school); 277 | 278 | Student student1= CloneUtil.clone(student); 279 | System.out.println(student.equals(student1)); 280 | System.out.println(student.school.equals(student1.school)); 281 | } 282 | } 283 | ``` 284 | 285 | 上面的结果均是`false`,说明确实不是同一个对象,发生了深拷贝。 286 | 287 | 288 | 289 | ### clone实现深拷贝 290 | 291 | 前面的`Student`和`School`都实现`Cloneable`接口,然后重写`clone()`方法: 292 | 293 | ```java 294 | 295 | public class Student implements Cloneable { 296 | String name; 297 | 298 | School school; 299 | 300 | public Student(String name, School school) { 301 | this.name = name; 302 | this.school = school; 303 | } 304 | 305 | @Override 306 | protected Object clone() throws CloneNotSupportedException { 307 | Student student = (Student) super.clone(); 308 | student.school = (School) school.clone(); 309 | return student; 310 | } 311 | } 312 | ``` 313 | 314 | ```java 315 | 316 | public class School implements Cloneable { 317 | String name; 318 | 319 | public School(String name) { 320 | this.name = name; 321 | } 322 | 323 | @Override 324 | protected Object clone() throws CloneNotSupportedException { 325 | return super.clone(); 326 | } 327 | } 328 | ``` 329 | 330 | 测试类: 331 | 332 | ```java 333 | public class Test { 334 | public static void main(String[] args) throws Exception{ 335 | School school = new School("东方小学"); 336 | Student student =new Student("小明",school); 337 | 338 | Student student1= (Student) student.clone(); 339 | System.out.println(student.equals(student1)); 340 | System.out.println(student.school.equals(student1.school)); 341 | } 342 | } 343 | ``` 344 | 345 | 测试结果一样,同样都是`false`,也是发生了深拷贝。 346 | 347 | ## 总结 348 | 349 | 原型模式适用于创建对象需要很多步骤或者资源的场景,而不同的对象之间,只有一部分属性是需要定制化的,其他都是相同的,一般来说,原型模式不会单独存在,会和其他的模式一起使用。值得注意的是,拷贝分为浅拷贝和深拷贝,浅拷贝如果发生数据修改,不同对象的数据都会被修改,因为他们共享了元数据。 -------------------------------------------------------------------------------- /设计模式/设计模式【6.1】--初探适配器模式.md: -------------------------------------------------------------------------------- 1 | 开局一张图,剩下全靠写... 2 | 3 | 4 | 5 | ## 介绍 6 | 7 | > 适配器模式(百度百科):在计算机编程中,适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。 8 | 9 | 适配器模式的主要目的就是为了**兼容性**,把原来不匹配的两个类或者接口可以协同工作,它属于结构型模式,主要分为三种:类适配器,对象适配器,接口适配器。 10 | 11 | 适配器模式灵活性比较好,可以提高复用性,但是如果滥用,系统调用关系会比较复杂,**每一次的适配,本质上都是一种妥协**。 12 | 13 | 不断妥协,最后迎来的,必定是重构。 14 | 15 | ## 适配器模式类型 16 | 17 | ### 类适配器 18 | 19 | 描述:适配器的类(`Adapter`),通过继承原有类,同时实现目标接口,完成的功能是拥有原有类的属性方法,同时可以调用目标接口。 20 | 例子:原来一种充电器(目标类)可以给`IPhone`充电,另一种充电器(接口)可以给`Android`手机充电,我们想实现一种适配器可以让`IPhone`充电器拥有`Android`充电器的功能。 21 | 22 | 代码结构如下: 23 | 24 | ![](https://img-blog.csdnimg.cn/img_convert/6c973063f2a1984a6191a3c627c2554f.png) 25 | 26 | - `AndroidCharger.class`: 27 | ```java 28 | //给android充电的接口 29 | public interface AndroidCharger { 30 | public void androidout(); 31 | } 32 | 33 | ``` 34 | - `AppleCharger.class` 35 | ```java 36 | //给苹果充电的类 37 | public class AppleCharger { 38 | public void iphoenOut(){ 39 | System.out.println("我是充电器,我可以给苹果充电..."); 40 | } 41 | } 42 | 43 | ``` 44 | - `ChagerAdapater.class` 45 | ```java 46 | //充电适配器 47 | public class ChagerAdapater extends AppleCharger implements AndroidCharger { 48 | @Override 49 | public void androidout() { 50 | iphoenOut(); 51 | System.out.println("适配器开始工作----"); 52 | System.out.print("我拥有了给Android充电的能力"); 53 | } 54 | } 55 | ``` 56 | - `Test.class` 57 | ```java 58 | public class Test { 59 | public static void main(String[]args){ 60 | ChagerAdapater chagerAdapater = new ChagerAdapater(); 61 | chagerAdapater.androidout(); 62 | } 63 | } 64 | ``` 65 | 运行结果如下: 66 | 67 | ![](https://img-blog.csdnimg.cn/img_convert/c8c05af931b51d22a6195215a13fbb50.png) 68 | 69 | > 个人理解:这里之所以一个继承一个接口,是因为java只能单继承,要去适配多个类,只能一个继承,一个用接口实现,有一定局限性。重写它的方法,这也比较灵活,可以对接口方法进行修改。 70 | 71 | ### 2.对象适配器 72 | 73 | 个人理解:上面所说的类适配器是通过继承与实现接口的方式实现(所继承的父类以及接口都是一个`class`),对象适配器就是根据“合成复用原则”,**不使用继承关系**,而是使用了**关联关系**,直接把另一个类的对象当成**成员对象**,也就是持有之前需要继承的类的实例。 74 | 代码结构没有改变,只是重新创建了一个包: 75 | 76 | ![](https://img-blog.csdnimg.cn/img_convert/e2c6f37e1bd1c41064a5237a8da8c989.png) 77 | 78 | - 更改后的 `ChagerAdapater.class` 79 | ```java 80 | //充电适配器 81 | public class ChagerAdapater implements AndroidCharger { 82 | //持有苹果充电器的实例 83 | private AppleCharger appleCharger; 84 | //构造器 85 | public ChagerAdapater(AppleCharger appleCharger){ 86 | this.appleCharger = appleCharger; 87 | } 88 | @Override 89 | public void androidout() { 90 | System.out.println("适配器开始工作----"); 91 | System.out.print("我拥有了给Android充电的能力"); 92 | } 93 | } 94 | ``` 95 | - 更改后的 Test.class 96 | ```java 97 | public class Test { 98 | public static void main(String[]args){ 99 | ChagerAdapater chagerAdapater = new ChagerAdapater(new AppleCharger()); 100 | chagerAdapater.androidout(); 101 | } 102 | } 103 | ``` 104 | 运行结果没有改变: 105 | 106 | ![](https://img-blog.csdnimg.cn/img_convert/c8c05af931b51d22a6195215a13fbb50.png) 107 | 108 | > * 个人理解:这个和第一种类的适配器其实思想上差不多,只是实现的方式不一样,类适配器是通过继承类,实现接口,对象适配器是把要继承的类变成了**属性对象**,把实例与适配器关联起来,也就是适配器的类持有了原有父类的对象实例。一般而言,由于`java`是单继承,所以我们尽量不要把这一次使用继承的机会给浪费了,这样写也比较灵活。 109 | 110 | ### 3.接口适配器 111 | 112 | 接口适配器,也可以称为默认适配器模式,或者缺省适配器模式。当我们不需要全部实现接口所实现的方法的时候,我们可以设计一个抽象类去实现接口,然后再这个抽象类中为所有方法提供一个默认的实现,这个抽象类的子类就可以有选择地对方法进行实现了。 113 | 代码结构如下: 114 | 115 | ![](https://img-blog.csdnimg.cn/img_convert/7870f75bda11b2bd41387e80d37b5cb9.png) 116 | 117 | 解释:`学生类`可以吃饭,学习,但是`教师类`也吃饭,但是教师不是学习,而是教书,所以我们把`学习`,`吃饭`,`教书`当成接口的方法,由于不是所有的类都需要实现所有接口,我们在中间实现了一个抽象类实现这些接口,所有的方法都提供了一个默认是实现方法。然后学生类或者教师类才去继承抽象类,从而实现自己所需要的一部分方法即可。 118 | 119 | 120 | - `myInterface.class` 121 | 122 | ```java 123 | //定义接口的方法 124 | public interface myInterface { 125 | //学习的接口方法 126 | public void study(); 127 | //教书的接口方法 128 | public void teach(); 129 | //吃饭的接口方法 130 | public void eat(); 131 | 132 | } 133 | ``` 134 | - `myAbstractClass.class(抽象类)` 135 | ```java 136 | public abstract class myAbstractClass implements myInterface{ 137 | //学习的接口方法 138 | @Override 139 | public void study(){} 140 | @Override 141 | //吃饭的接口方法 142 | public void eat(){} 143 | //教书的接口方法 144 | @Override 145 | public void teach(){} 146 | } 147 | 148 | ``` 149 | - `Student.class(学生类)` 150 | ```java 151 | public class Student extends myAbstractClass{ 152 | //学习的接口方法 153 | @Override 154 | public void study(){ 155 | System.out.println("我是学生,我要好好学习"); 156 | } 157 | @Override 158 | //吃饭的接口方法 159 | public void eat(){ 160 | System.out.println("我是学生,我要吃饭"); 161 | } 162 | } 163 | 164 | ``` 165 | - `Teacher.class(教师类)` 166 | ```java 167 | public class Teacher extends myAbstractClass { 168 | @Override 169 | //吃饭的接口方法 170 | public void eat(){ 171 | System.out.println("我是教师,我要吃饭"); 172 | } 173 | //教书的接口方法 174 | @Override 175 | public void teach(){ 176 | System.out.println("我是教师,我要教育祖国的花朵"); 177 | } 178 | } 179 | 180 | ``` 181 | - `Test.calss(测试类)` 182 | ```java 183 | public class Test { 184 | public static void main(String[] args){ 185 | Student student = new Student(); 186 | Teacher teacher = new Teacher(); 187 | student.eat(); 188 | student.study(); 189 | teacher.eat(); 190 | teacher.teach(); 191 | } 192 | } 193 | 194 | ``` 195 | 运行的结果: 196 | 197 | ![](https://img-blog.csdnimg.cn/img_convert/06c2a3fd52aed7fa63e683b93aa0139e.png) 198 | 199 | #### 4.总结 200 | 1.类适配器模式主要是通过继承目标的类,实现要增加的接口方法,就可以把类与接口适配一起工作。 201 | 202 | 2.对象的适配器与类适配器功能大致一样,但是为了更加灵活,不再使用继承的方式,而是直接使用了成员变量这样的方法,将目标的类的对象持有,再实现要增加的接口方法,达到一样的目的。 203 | 204 | 3.接口适配器模式,是把所有的方法定义到一个接口里面,然后创建一个抽象类去实现所有的方法,再使用真正的类去继承抽象类,只需要重写需要的方法,就可以完成适配的功能。 205 | 如果有兴趣,可以了解一下另外一种说法的适配器模式[https://blog.csdn.net/Aphysia/article/details/80292049] 206 | 207 | 4.建议尽量使用对象的适配器模式,少用继承。适配器模式也是一种包装模式,它与装饰模式同样具有包装的功能,此外,对象适配器模式还具有委托的意思。总的来说,适配器模式属于补偿模式,专门用来在系统后期扩展、修改时使用,但要注意不要过度使用适配器模式。 -------------------------------------------------------------------------------- /设计模式/设计模式【6.2】--再谈适配器模式.md: -------------------------------------------------------------------------------- 1 | **这里说的适配器不是通常所说的类适配器,对象适配器,接口适配器,这里实现的是把所有的类进行统一管理的适配器。如需要查找设计模式的三种主要适配器模式,请点击**https://blog.csdn.net/Aphysia/article/details/80291916 2 | 3 | 4 | 5 | 适配器模式(百度百科):在计算机编程中,适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。 6 | 7 | 可以这么理解,原来可能两个接口或者两个类不兼容,适配器模式要做的事情就是把它们统一管理,让他们可以一起工作。举个简单的例子:内存卡和笔记本,是不能直接连接工作的,但是我们使用读卡器,相当于适配器,把它们连接起来了。 8 | 9 | ##### 1.**不使用适配器**的例子: 10 | -   需求:程序猿的工作是`program()`,教师的工作是`teach()`,那么这些不同的职业,具体的工作都是不一样的,这些程序猿program()方法内容也可能是不一样的,比如说京东,阿里,腾讯等等,教师也是一样的,不同学校的老师工作内容也各异。所以我们必须定义接口,不同工作内容的也可以通过实现自己的接口去实现。
11 | 代码结果如下: 12 | 13 | ![](https://img-blog.csdnimg.cn/img_convert/90efc43becdae94d0c74b20b2c168ccb.png) 14 | 15 | **  IProgramer.class**(程序猿撸代码的接口) 16 | 17 | ```java 18 | package com.noadapter; 19 | 20 | public interface IProgramer { 21 | public void program(); 22 | } 23 | 24 | ``` 25 | **  Programer.class**(程序猿的类,实现了撸代码的接口) 26 | ```java 27 | package com.noadapter; 28 | 29 | public class Programer implements IProgramer { 30 | @Override 31 | public void program() { 32 | System.out.println("我是一个优秀的程序猿,我整天撸代码"); 33 | } 34 | } 35 | 36 | ``` 37 |   下面的教师接口以及实现教师的类也和上面程序猿的一样: 38 | 39 | **ITeacher.class**(教师教书接口): 40 | 41 | ```java 42 | package com.noadapter; 43 | 44 | public interface ITeacher { 45 | public void teach(); 46 | } 47 | 48 | ``` 49 |   **Teacher.class**(实现了教书的教师类): 50 | ```java 51 | 52 | package com.noadapter; 53 | 54 | public class Teacher implements ITeacher { 55 | @Override 56 | public void teach() { 57 | System.out.println("我是教师,我教育祖国的花朵"); 58 | } 59 | } 60 | ``` 61 |   **MyTest.class** 测试类: 62 | ```java 63 | package com.noadapter; 64 | 65 | public class MyTest { 66 | public static void main(String []args){ 67 | ITeacher teacher = new Teacher(); 68 | IProgramer programer = new Programer(); 69 | //必须挨个访问他们的方法 70 | teacher.teach(); 71 | programer.program(); 72 | } 73 | } 74 | 75 | ``` 76 | 运行结果: 77 | 78 | ![](https://img-blog.csdnimg.cn/img_convert/f843b7af19ac33587f8b27aa0f3c1fe0.png) 79 | 80 | 理解:如果不是用适配器模糊,那么我们要定义出所有的工种对象(程序猿,教师等等),还要为他们实现各自的接口,然后对他们的方法进行调用,这样有多少个工种,就要写多少个方法调用,比较麻烦。 81 | 82 | ##### 2.只定义一个适配器实现类 83 | 在前面的基础上修改,增加了`IWorkAdapter.class`以及它的实现类`WorkerAdapter.class`,以及更改了测试方法,其他的都没有改变,代码结构如下: 84 | 85 | ![](https://img-blog.csdnimg.cn/img_convert/4fc3a1b984637ee6ff831b02b95dc8b7.png) 86 | 87 | 88 | 89 | ![](https://img-blog.csdnimg.cn/img_convert/86ad6c62a2586c362b25db515f68ad1d.png) 90 | 91 | 增加的`IWorkAdapter.class`(适配器的接口): 92 | 93 | ```java 94 | public interface IWorkAdapter { 95 | //参数之所以是Object,是因为要兼容所有的工种对象 96 | public void work(Object worker); 97 | } 98 | ``` 99 | 增加的`WorkAdapter.class`(适配器的类): 100 | ```java 101 | 102 | public class WorkAdaper implements IWorkAdapter { 103 | @Override 104 | public void work(Object worker) { 105 | if(worker instanceof IProgramer){ 106 | ((IProgramer) worker).program(); 107 | } 108 | if(worker instanceof ITeacher){ 109 | ((ITeacher) worker).teach(); 110 | } 111 | } 112 | } 113 | 114 | ``` 115 | 更改过的测试类`MyTest.class`: 116 | ```java 117 | public class MyTest { 118 | public static void main(String []args){ 119 | ITeacher teacher = new Teacher(); 120 | IProgramer programer = new Programer(); 121 | //把两个工种放到对象数组 122 | Object[] workers = {teacher,programer}; 123 | //定义一个适配器 124 | IWorkAdapter adapter = new WorkAdaper(); 125 | //适配器遍历对象 126 | for(Object worker:workers){ 127 | adapter.work(worker); 128 | } 129 | } 130 | } 131 | 132 | ``` 133 | 结果依然不变: 134 | 135 | 136 | 137 | ![](https://img-blog.csdnimg.cn/img_convert/f843b7af19ac33587f8b27aa0f3c1fe0.png) 138 | 139 | 分析:只写一个适配器,功能上就像是把接口集中到一起,在中间加了一层,这一层把调用不同工种(程序猿,教师)之间的差异屏蔽掉了,这样也达到了解耦合的作用。 140 | 141 | ##### 3.多个适配器的模式 142 | 也就是为每一个工种都定义一个适配器(在一个适配器的基础上进行修改) 143 | 144 | ![](https://img-blog.csdnimg.cn/img_convert/595b1be259236220fccf2042ad938067.png) 145 | 146 | 修改 **IWorkAdapter.class** 147 | 148 | ```java 149 | public interface IWorkAdapter { 150 | //参数之所以是Object,是因为要兼容所有的工种对象 151 | public void work(Object worker); 152 | //判断当前的适配器是否支持指定的工种对象 153 | boolean supports(Object worker); 154 | } 155 | ``` 156 | 定义一个**TeacherAdapter.class** 157 | ```java 158 | public class TeacherAdapter implements IWorkAdapter{ 159 | @Override 160 | public void work(Object worker) { 161 | ((ITeacher)worker).teach(); 162 | } 163 | 164 | @Override 165 | public boolean supports(Object worker) { 166 | return (worker instanceof ITeacher); 167 | } 168 | } 169 | ``` 170 | 定义一个**ProgrammerAdapter.class** 171 | ```java 172 | public class ProgrammerAdapter implements IWorkAdapter{ 173 | @Override 174 | public void work(Object worker) { 175 | ((IProgramer)worker).program(); 176 | 177 | } 178 | @Override 179 | public boolean supports(Object worker) { 180 | return (worker instanceof IProgramer); 181 | } 182 | } 183 | 184 | ``` 185 | 测试类(`Test.class`): 186 | ```java 187 | public class MyTest { 188 | public static void main(String []args){ 189 | ITeacher teacher = new Teacher(); 190 | IProgramer programer = new Programer(); 191 | //把两个工种放到对象数组 192 | Object[] workers = {teacher,programer}; 193 | //适配器遍历对象 194 | for(Object worker:workers){ 195 | IWorkAdapter adapter = getAdapter(worker); 196 | adapter.work(worker); 197 | } 198 | } 199 | public static IWorkAdapter getAdapter(Object object){ 200 | IWorkAdapter teacherAdapter = new TeacherAdapter(); 201 | IWorkAdapter programmerAdapter = new ProgrammerAdapter(); 202 | IWorkAdapter[] adapters = {teacherAdapter,programmerAdapter}; 203 | for(IWorkAdapter adapter:adapters){ 204 | if(adapter.supports(object)){ 205 | return adapter; 206 | } 207 | } 208 | return null; 209 | } 210 | } 211 | ``` 212 | 个人理解:其实多个适配器的根本是去获取支持该对象的适配器,通过该适配器来使用这个对象。 213 | -------------------------------------------------------------------------------- /设计模式/设计模式【7】--桥接模式.md: -------------------------------------------------------------------------------- 1 | 设计模式,写代码必备神器... 2 | 3 | 4 | 5 | ## 桥接模式是什么? 6 | 7 | **桥接模式**是把抽象化和实现化解耦,让两者可以独立,该设计模式属于结构性设计模式。何为将抽象化和实现化解耦,可以理解为将功能点抽象出来,功能的实现如何取决于不同的需求,但是抽象的功能点(接口)已经**被桥接**到原本的类型上,只用关注与实现。原本的类型变化,和抽象的功能点可以自由变化,**中间的桥梁已经搭建起来了**。 8 | 9 | 桥接模式其实就是不单单使用类继承的方式,而是重点使用类聚合的方式,进行桥接,把抽象的功能点,聚合(注入)到基类里面。 10 | 11 | ## 桥接模式的好处 12 | 13 | 14 | 15 | **一般用于解决什么问题呢?** 16 | 17 | 主要是功能点实现种类多,多个维度的功能点,**独立变化**,没有什么关联,可以按照维度来管理。比如有 2 个维度,每个维度有 3 种实现,但是不同的维度之间其实没有关联,如果按照维度之间两两关联来搞,单单是实现类的数量就已经`2 * 3 = 6`个类了,是在不太合适,还耦合在一块。 18 | 19 | 20 | 21 | 用电脑举个例子,既会分成不同的品牌,比如戴尔,联想,又会分为台式机,笔记本,那么不同的类就会很多,功能可能比较重复。正是鉴于这一点,我们得剥离重复的功能,用桥接的方式,来维护抽象出来的共同功能点。 22 | 23 | ![image-20211204132503297](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20211204132503297.png) 24 | 25 | 如果再新增一个品牌,比如,华硕,那么又得增加两个类,这明显不太合适,不同的类很多功能可能会重复。 26 | 27 | ![image-20211204131258227](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20211204131258227.png) 28 | 29 | 那么桥接模式怎么处理呢?桥接模式把两个不同的维度 **台式机** 和 **笔记本**抽取出来,相当于作为一个通用的属性来维护。 30 | 31 | ![image-20211205224859234](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20211205224859234.png) 32 | 33 | ## 代码Demo演示 34 | 35 | 首先,定义一个抽象的电脑类`AbstractComputer`,在其中有一个属性是`ComputerType`,表示电脑的类型: 36 | 37 | ```java 38 | public abstract class AbstractComputer { 39 | 40 | protected ComputerType type; 41 | 42 | public void setType(ComputerType type) { 43 | this.type = type; 44 | } 45 | 46 | public abstract void work(); 47 | } 48 | ``` 49 | 50 | 再定义三种类型的电脑:`LenovoComputer`,`AsusComputer`,`DellComputer`: 51 | 52 | ```java 53 | public class LenovoComputer extends AbstractComputer{ 54 | 55 | @Override 56 | public void work() { 57 | System.out.print("联想"); 58 | type.feature(); 59 | } 60 | } 61 | ``` 62 | 63 | ```java 64 | public class AsusComputer extends AbstractComputer{ 65 | @Override 66 | public void work() { 67 | System.out.print("华硕"); 68 | type.feature(); 69 | } 70 | } 71 | ``` 72 | 73 | ```java 74 | public class DellComputer extends AbstractComputer{ 75 | @Override 76 | public void work() { 77 | System.out.print("戴尔"); 78 | type.feature(); 79 | } 80 | } 81 | ``` 82 | 83 | 电脑类型这个维度同样需要一个抽象类`ComputerType`,里面有一个说明功能的方法`feature()`: 84 | 85 | ```java 86 | public abstract class ComputerType { 87 | // 功能特性 88 | public abstract void feature(); 89 | } 90 | ``` 91 | 92 | 电脑类型这个维度,我们定义台式机和笔记本电脑两种: 93 | 94 | ```java 95 | public class DesktopComputerType extends ComputerType{ 96 | @Override 97 | public void feature() { 98 | System.out.println(" 台式机:性能强大,拓展性强"); 99 | } 100 | } 101 | ``` 102 | 103 | ```java 104 | public class LaptopComputerType extends ComputerType{ 105 | @Override 106 | public void feature() { 107 | System.out.println(" 笔记本电脑:小巧便携,办公不在话下"); 108 | } 109 | } 110 | ``` 111 | 112 | 测试一下,我们需要不同的搭配的时候,只需要将一个维度`set`到对象中去即可,就可以聚合出不同品牌不同类型的电脑: 113 | 114 | ```java 115 | public class BridgeTest { 116 | public static void main(String[] args) { 117 | ComputerType desktop = new DesktopComputerType(); 118 | LenovoComputer lenovoComputer = new LenovoComputer(); 119 | lenovoComputer.setType(desktop); 120 | lenovoComputer.work(); 121 | 122 | ComputerType laptop = new LaptopComputerType(); 123 | DellComputer dellComputer = new DellComputer(); 124 | dellComputer.setType(laptop); 125 | dellComputer.work(); 126 | } 127 | } 128 | ``` 129 | 130 | 测试结果: 131 | 132 | ```txt 133 | 联想 台式机:性能强大,拓展性强 134 | 戴尔 笔记本电脑:小巧便携,办公不在话下 135 | ``` 136 | 137 | ## 总结一下 138 | 139 | 桥接模式,本质上就是将不同维度或者说功能,抽象出来,作为属性,聚合到对象里面,而不是通过继承。这样一定程度上减少了类的数量,但是如果不同的维度之间,变化是相关联的,这样使用起来还需要再做各种特殊判断,使用起来容易造成混乱,不宜使用。(重点:**用组合/聚合关系代替继承关系来实现**) 140 | 141 | 142 | 143 | `JDBC`,搞过`Java`的同学应该都知道,这是一种`Java`统一访问数据库的`API`,可以操作`Mysql`,`Oracle`等,主要用到的设计模式也是桥接模式,有兴趣可以了解一下`Driver`驱动类管理的源码。 144 | 145 | -------------------------------------------------------------------------------- /设计模式/设计模式【8】--装饰器模式.md: -------------------------------------------------------------------------------- 1 | 2 | ![https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/设计模式.png](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/设计模式.png) 3 | ## 装饰器模式 4 | 前面学习了好几种设计模式,今天继续... 5 | 6 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211207234634.png) 7 | 8 | 装饰器模式,属于结构型模式,用来包裹封装现在的类对象,希望可以在不修改现在类对象和类定义的前提下,能够拓展对象的功能。 9 | 10 | 调用的时候,使用的是装饰后的对象,而不是原对象。,提供了额外的功能。 11 | 12 | 不知道大家有没有看手工耿的自制钢琴烤串车视频【https://www.bilibili.com/video/BV1334y1Z7kq?spm_id_from=333.999.0.0 】, 本来是一个钢琴,但是为了边弹琴,边烤串,改造装饰了一下,变成了特殊的钢琴,提供了额外的烤串功能。**典型的装饰器模式** 13 | 14 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211207233928.png) 15 | 16 | **目的:** 为了灵活的拓展类对象的功能。 17 | 18 | 主要包括以下几种角色: 19 | - 抽象组件(`Component`): 被装饰的原始类的抽象,可以是抽象类,亦或是接口。 20 | - 具体实现类(`ConcreteComponent`):具体的被抽象的类 21 | - 抽象装饰器(`Decorator`): 通用抽象器 22 | - 具体装饰器(`ConcreteDecorator`):`Decorator` 的具体实现类,理论上,每个 `ConcreteDecorator`都扩展了`Component`对象的一种功能。 23 | 24 | ## 优缺点 25 | 26 | 优点: 27 | - 相对于类继承,包裹对象更加容易拓展,更加灵活 28 | - 装饰类和被装饰类相互独立,耦合度比较低 29 | - 完全遵守开闭原则。 30 | 31 | 缺点: 32 | - 包裹对象层级较深的时候,理解难度较大。 33 | 34 | ## 实现 35 | 36 | 先抽闲一个乐器接口类 `Instrument`: 37 | ```java 38 | public interface Instrument { 39 | void play(); 40 | } 41 | ``` 42 | 43 | 弄两种乐器`Piano`和`Guitar` ,实现乐器接口: 44 | 45 | ```java 46 | public class Piano implements Instrument{ 47 | @Override 48 | public void play() { 49 | System.out.println("手工耿弹奏钢琴"); 50 | } 51 | } 52 | ``` 53 | 54 | ```java 55 | public class Guitar implements Instrument{ 56 | @Override 57 | public void play() { 58 | System.out.println("手工耿弹吉他"); 59 | } 60 | } 61 | ``` 62 | 63 | 64 | 不管手工耿要边弹吉他边烧烤,还是边弹钢琴边烧烤,还是边弹钢琴边洗澡,不管什么需求,我们抽象一个装饰器类,专门对乐器类进行包装,装饰。 65 | 66 | ```java 67 | public class InstrumentDecorator implements Instrument{ 68 | protected Instrument instrument; 69 | 70 | public InstrumentDecorator(Instrument instrument) { 71 | this.instrument = instrument; 72 | } 73 | 74 | @Override 75 | public void play() { 76 | instrument.play(); 77 | } 78 | } 79 | ``` 80 | 81 | 上面的是抽象的装饰类,具体装饰成什么样,我们得搞点实际动作,那就搞个烧烤功能。 82 | 83 | ```java 84 | public class BarbecueInstrumentDecorator extends InstrumentDecorator { 85 | public BarbecueInstrumentDecorator(Instrument instrument) { 86 | super(instrument); 87 | } 88 | 89 | @Override 90 | public void play() { 91 | instrument.play(); 92 | barbecue(); 93 | } 94 | 95 | public void barbecue(){ 96 | System.out.println("手工耿在烧烤"); 97 | } 98 | } 99 | ``` 100 | 101 | 102 | 测试一下: 103 | 104 | ```java 105 | public class DecoratorDemo { 106 | public static void main(String[] args) { 107 | Instrument instrument = new Piano(); 108 | instrument.play(); 109 | System.out.println("----------------------------------------"); 110 | InstrumentDecorator barbecuePiano = new BarbecueInstrumentDecorator(new Piano()); 111 | barbecuePiano.play(); 112 | System.out.println("----------------------------------------"); 113 | InstrumentDecorator barbecueGuitar = new BarbecueInstrumentDecorator(new Guitar()); 114 | barbecueGuitar.play(); 115 | } 116 | } 117 | ``` 118 | 119 | 测试结果如下,可以看到不装饰的时候,只能干一件事,装饰之后的对象,既可以弹奏乐器,也可以烧烤,不禁感叹:原来手工耿是设计模式高手: 120 | 121 | ```txt 122 | 手工耿弹奏钢琴 123 | ---------------------------------------- 124 | 手工耿弹奏钢琴 125 | 手工耿在烧烤 126 | ---------------------------------------- 127 | 手工耿弹吉他 128 | 手工耿在烧烤 129 | ``` 130 | 131 | ## 小结一下 132 | 133 | 设计模式,不是银弹,只是在软件工程或者说编程中,演变出来的较好实践。我们不能为了设计模式而设计模式,学习理论只是为了更好的使用它,知道什么时候应该使用,什么时候不该使用。 134 | 135 | 装饰器模式是为了拓展其功能,但又不破坏原来的结构的前提下做的,其实在`Java IO`的源码里面有大量使用,比如: 136 | ```java 137 | DataInputStream dis = new DataInputStream( 138 | new BufferedInputStream( 139 | new FileInputStream("test.txt") 140 | ) 141 | ); 142 | ``` 143 | 144 | 先把`FileInputStream`传递给`BufferedInputStream`弄成一个装饰对象,再把装饰对象传递给`DataInputStream`,再装饰一遍。最终,`FileInputStream` 被包装成了 `DataInputStream`,感兴趣的同学可以翻一下源码。 145 | 146 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211208085437.png) -------------------------------------------------------------------------------- /设计模式/设计模式【9】--外观模式.md: -------------------------------------------------------------------------------- 1 | ![1](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/设计模式.png) 2 | 3 | 开局一张图,剩下全靠写... 4 | 5 | 6 | 7 | ## 外观模式是什么 8 | 9 | 外观模式,其实是用来隐藏系统的复杂性的,屏蔽掉了背后复杂的逻辑,向用户提供简单的可以访问系统的接口,也是属于结构型模式的一种 。 10 | 11 | 12 | 13 | 举个例子,比如我们的`Java` 三层`MVC`架构,对外提供的是`controller`,但是`controller`内部可能调用了很多`service`,`service`又调用了一些`mapper`,反正就是内部很复杂,但是对外只是一个接口,一个门面,外部看起来是简单的,外观很好看,实际上,你都懂。 14 | 15 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/a4314a35ly1fjs50be9b1g204l036avh.gif) 16 | 17 | 再举个栗子,我们用的电脑,其实内部也是极其复杂的,但是我们操作的时候,已经不管内存,cpu,磁盘,显卡这些怎么工作了,甚至更加底层还有二进制,硬件之类的,我们只需要开机,做我们想做的事情,比如`Ctrl+C`,`Ctrl+V`,在美丽漂亮的界面上操作就可以了。 18 | 19 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/006APoFYly8gwdwan760qg30b404tan8.gif) 20 | 21 | ### 外观模式的角色 22 | 23 | 外观模式主要包括几个角色: 24 | 25 | - 外观角色:糅合多个子系统功能,对外提供一个共同的接口 26 | - 子系统的角色:实现系统的部分功能 27 | - 客户角色:通过外观角色访问各个子系统的功能 28 | 29 | 30 | 31 | ### 优点与缺点 32 | 33 | 优点: 34 | 35 | - 减少系统依赖,这里指的是对外的系统依赖 36 | - 提高灵活性 37 | - 提高安全性 38 | 39 | 40 | 41 | 缺点: 42 | 43 | - 把东西糅合到一个人身上,带来未知的风险 44 | - 增加新的子系统可能需要修改外观类或者客户端的源代码,违反了“开闭原则” 45 | 46 | 47 | 48 | ## 测试例子 49 | 50 | 我们以电脑为例子,先给电脑的每个部件抽象定义成为一个组件,赋予一个`work()`的方法: 51 | 52 | ```java 53 | public interface Component { 54 | public void work(); 55 | } 56 | ``` 57 | 58 | 再定义内存,磁盘,cpu三种不同组件,分别实现上面的接口,各自工作: 59 | 60 | ```java 61 | public class Disk implements Component{ 62 | @Override 63 | public void work() { 64 | System.out.println("磁盘工作了..."); 65 | } 66 | } 67 | 68 | public class CPU implements Component{ 69 | @Override 70 | public void work() { 71 | System.out.println("CPU工作了..."); 72 | } 73 | } 74 | 75 | 76 | public class Memory implements Component{ 77 | @Override 78 | public void work() { 79 | System.out.println("内存工作了..."); 80 | } 81 | } 82 | ``` 83 | 84 | 然后以上组件可能是交叉在一起工作的,我们模拟一下开机过程,操作系统分别调用他们: 85 | 86 | ```java 87 | public class OperationSystem { 88 | private Component disk; 89 | private Component memory; 90 | private Component CPU; 91 | 92 | public OperationSystem() { 93 | this.disk = new Disk(); 94 | this.memory = new Memory(); 95 | this.CPU = new CPU(); 96 | } 97 | 98 | public void startingUp(){ 99 | System.out.println("准备开机..."); 100 | disk.work(); 101 | memory.work(); 102 | CPU.work(); 103 | } 104 | } 105 | ``` 106 | 107 | 而使用人调用的其实是操作系统的开机启动方法,不会直接调用到内部的方法,也就是屏蔽掉了所有的细节: 108 | 109 | ```java 110 | public class PersonTest { 111 | public static void main(String[] args) { 112 | OperationSystem operationSystem = new OperationSystem(); 113 | operationSystem.startingUp(); 114 | } 115 | } 116 | ``` 117 | 118 | 执行结果如下: 119 | 120 | ```txt 121 | 准备开机... 122 | 磁盘工作了... 123 | 内存工作了... 124 | CPU工作了... 125 | ``` 126 | 127 | 最后简单小结一下,外观模式,可以成为门面模式,也就是屏蔽掉内部细节,只对外提供接口,实现所需的功能,内部功能可能很复杂,以上我们模拟的只是简单操作。学会了么? 128 | 129 | ![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211213000738.png) 130 | --------------------------------------------------------------------------------