diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000..5008ddfcf5 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 815e8cc827..c6fcbf307e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,7 @@ node_modules .cache .temp +dist package-lock.json -.DS_Store -dump.rdb -docs/.vuepress/.cache/ -docs/.vuepress/.temp/ -docs/dist/ -dist.zip -images -*.log -.yarn -*-vip.md -/.vscode +yarn.lock +package.json diff --git a/README.md b/README.md index efb70212e3..7d80dc033b 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,27 @@ +👉 沉默王二-《Java 程序员进阶之路》官方知识星球来啦!!! + +如果你需要专属Java学习/面试小册/一对一交流/简历修改/专属求职指南/学习打卡,不妨花 3 分钟左右看看星球的详细介绍:沉默王二-《Java 程序员进阶之路》详细介绍 (一定要确定自己真的需要再加入,一定要看完详细介绍之后再加我)。

- - 二哥的Java进阶之路 + + Java 程序员进阶之路

- - - - 无套路下载 - 二哥的Java进阶之路

- Github | - Gitee + + + + + 无套路下载 +

+ # 为什么会有这个开源知识库 -> 知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是我自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 二哥的Java进阶之路😄。 +> [!NOTE] +> 知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 Java 程序员进阶之路😄。 > > 知识库旨在为学习 Java 的小伙伴提供一系列: > - **优质的原创 Java 教程** @@ -28,578 +32,435 @@ > > 赠人玫瑰手有余香。知识库会持续保持**更新**,欢迎收藏品鉴! > -> **转载须知** :以下所有文章如非文首说明为转载皆为我(沉默王二)的原创,且不允许转载,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! +> **转载须知** :以下所有文章如非文首说明为转载皆为我(沉默王二)的原创,转载在文首注明出处,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! > > 推荐你通过在线阅读网站进行阅读,体验更好,速度更快! > -> - [**二哥的Java进阶之路在线网站(新域名:javabetter.cn 好记,推荐👍)**](https://javabetter.cn) -> - [老版 Java 程序员进阶之路在线网址(老域名 tobebetterjavaer.com 难记)](https://tobebetterjavaer.com) -> - [技术派之二哥的Java进阶之路专栏](https://paicoding.com/column/5/1) -> -> 如果你更喜欢离线的 PDF 版本,戳这个链接获取[👍二哥的 Java 进阶之路.pdf](docs/src/overview/readme.md) +> - [Java 程序员进阶之路在线阅读网站(VuePress 版)](https://tobebetterjavaer.com/) +> - [Java 程序员进阶之路在线阅读网站(docsify 版)](https://docsify.tobebetterjavaer.com/) +> +> 建议给本仓库点个 star,满足一下我的虚荣心,内容质量也绝对值得你一个 star。我还在继续创作,给我一点继续更新的动力,笔芯。 + # 知识库地图 +> [!NOTE] > 知识库收录的核心内容就全在这里面了,大类分为 Java 核心、Java 企业级开发、数据库、计算机基础、求职面试、学习资源、程序人生,几乎你需要的这里都有。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/tobebetterjavaer-map.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/tobebetterjavaer-map.png) -一个人可以走得很快,但一群人才能走得更远。[二哥的编程星球](https://javabetter.cn/zhishixingqiu/)已经有 **10000 多名** 球友加入了(马上涨价到 169 元,抓紧时间趁没涨价前加入吧),如果你也需要一个优质的学习环境,扫描下方的优惠券加入我们吧。 -

- - - 星球优惠券 - - -

+# 学习路线 -新人可免费体验 3 天,不满意可全额退款(只能帮你到这里了😄)。 +>[!NOTE] +> 除了 Java 学习路线,还有 C语言、C++、Python、Go 语言、操作系统、前端、蓝桥杯等硬核学习路线,欢迎收藏品鉴! + + * [Java学习路线一条龙版(建议收藏:+1:)](docs/xuexiluxian/java/yitiaolong.md) + * [Java并发编程学习路线(建议收藏:+1:)](docs/xuexiluxian/java/thread.md) + * [Java虚拟机学习路线(建议收藏:+1:)](docs/xuexiluxian/java/jvm.md) + * [C语言学习路线(建议收藏:+1:)](docs/xuexiluxian/c.md) + * [C++学习路线(建议收藏:+1:)](docs/xuexiluxian/ccc.md) + * [Python学习路线(建议收藏:+1:)](docs/xuexiluxian/python.md) + * [Go语言学习路线(建议收藏:+1:)](docs/xuexiluxian/go.md) + * [操作系统学习路线(建议收藏:+1:)](docs/xuexiluxian/os.md) + * [前端学习路线(建议收藏:+1:)](docs/xuexiluxian/qianduan.md) + * [蓝桥杯学习路线(建议收藏:+1:)](docs/xuexiluxian/lanqiaobei.md) + * [算法和数据结构学习路线(建议收藏:+1:)](docs/xuexiluxian/algorithm.md) -这是一个 **简历精修 + 编程项目实战 + Java 面试指南 + LeetCode 刷题**的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的内容,**相信能帮助你走的更快、更稳、更远**。 +# 面渣逆袭 -- [二哥精修简历服务,让你投了就有笔试&面试✌️](https://javabetter.cn/zhishixingqiu/jianli.html) -- [二哥的RAG知识库项目派聪明上线了,AI时代你必须拥有的实战项目✌️](https://javabetter.cn/zhishixingqiu/paismart.html) -- [Go 版本的派聪明RAG知识库项目上线了,学习 Go 语言的小伙伴有福了✌️](https://javabetter.cn/zhishixingqiu/paismart-go.html) -- [二哥的技术派实战项目更新了,秋招&暑期/日常实习大杀器✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) -- [二哥的PmHub微服务实战项目上线了,校招和社招均可用✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) -- [二哥的Java面试指南专栏更新了,求职面试必备✌️](https://javabetter.cn/zhishixingqiu/mianshi.html) +> [!NOTE] +> **面试前必读系列**!包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis 等等。 +- [面渣逆袭(Java 基础篇)必看:+1:](docs/sidebar/sanfene/javase.md) +- [面渣逆袭(Java 集合框架篇)必看:+1:](docs/sidebar/sanfene/collection.md) +- [面渣逆袭(Java 并发编程篇)必看:+1:](docs/sidebar/sanfene/javathread.md) +- [面渣逆袭(Java 虚拟机篇)必看:+1:](docs/sidebar/sanfene/jvm.md) +- [面渣逆袭(Spring)必看:+1:](docs/sidebar/sanfene/spring.md) +- [面渣逆袭(Redis)必看:+1:](docs/sidebar/sanfene/redis.md) -# 学习路线 +# 学习建议 -> 除了 Java 学习路线,还有 MySQL、Redis、C语言、C++、Python、Go 语言、操作系统、前端、数据结构与算法、蓝桥杯、大数据、Android、.NET等硬核学习路线,欢迎收藏品鉴! - - * [Java学习路线一条龙版(建议收藏🔥)](docs/src/xuexiluxian/java/yitiaolong.md) - * [Java并发编程学习路线(建议收藏🔥)](docs/src/xuexiluxian/java/thread.md) - * [Java虚拟机学习路线(建议收藏🔥)](docs/src/xuexiluxian/java/jvm.md) - * [MySQL 学习路线(建议收藏🔥)](docs/src/xuexiluxian/mysql.md) - * [Redis 学习路线(建议收藏🔥)](docs/src/xuexiluxian/redis.md) - * [C语言学习路线(建议收藏🔥)](docs/src/xuexiluxian/c.md) - * [C++学习路线(建议收藏🔥)](docs/src/xuexiluxian/ccc.md) - * [Python学习路线(建议收藏🔥)](docs/src/xuexiluxian/python.md) - * [Go语言学习路线(建议收藏🔥)](docs/src/xuexiluxian/go.md) - * [操作系统学习路线(建议收藏🔥)](docs/src/xuexiluxian/os.md) - * [前端学习路线(建议收藏🔥)](docs/src/xuexiluxian/qianduan.md) - * [算法和数据结构学习路线(建议收藏🔥)](docs/src/xuexiluxian/algorithm.md) - * [蓝桥杯学习路线(建议收藏🔥)](docs/src/xuexiluxian/lanqiaobei.md) - * [大数据学习路线(建议收藏🔥)](docs/src/xuexiluxian/bigdata.md) - * [Android 安卓学习路线(建议收藏🔥)](docs/src/xuexiluxian/android.md) - * [.NET 学习路线(建议收藏🔥)](docs/src/xuexiluxian/donet.md) - * [Linux 学习路线(建议收藏🔥)](docs/src/xuexiluxian/linux.md) - +> [!NOTE] +> **收集了我所有的知乎高赞帖子**!全方位迷茫解惑。 -# 面渣逆袭 +- [如何阅读《深入理解计算机系统》这本书?](docs/xuexijianyi/read-csapp.md) +- [电子信息工程最好的出路的是什么?](docs/xuexijianyi/electron-information-engineering.md) -> **面试前必读系列**!包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis、MyBatis、MySQL、操作系统、计算机网络、RocketMQ、分布式、微服务、设计模式、Linux 等等。 -- [面渣逆袭(MySQL八股文面试题)必看👍](docs/src/sidebar/sanfene/mysql.md) -- [面渣逆袭(Redis八股文面试题)必看👍](docs/src/sidebar/sanfene/redis.md) -- [面渣逆袭(Spring八股文面试题)必看👍](docs/src/sidebar/sanfene/spring.md) -- [面渣逆袭(Java 基础篇八股文面试题)必看👍](docs/src/sidebar/sanfene/javase.md) -- [面渣逆袭(Java 集合框架篇八股文面试题)必看👍](docs/src/sidebar/sanfene/collection.md) -- [面渣逆袭(Java 并发编程篇八股文面试题)必看👍](docs/src/sidebar/sanfene/javathread.md) -- [面渣逆袭(Java 虚拟机篇八股文面试题)必看👍](docs/src/sidebar/sanfene/jvm.md) -- [面渣逆袭(MyBatis八股文面试题)必看👍](docs/src/sidebar/sanfene/mybatis.md) -- [面渣逆袭(操作系统八股文面试题)必看👍](docs/src/sidebar/sanfene/os.md) -- [面渣逆袭(计算机网络八股文面试题)必看👍](docs/src/sidebar/sanfene/network.md) -- [面渣逆袭(RocketMQ八股文面试题)必看👍](docs/src/sidebar/sanfene/rocketmq.md) -- [面渣逆袭(分布式面试题八股文)必看👍](docs/src/sidebar/sanfene/fenbushi.md) -- [面渣逆袭(微服务面试题八股文)必看👍](docs/src/sidebar/sanfene/weifuwu.md) -- [面渣逆袭(设计模式面试题八股文)必看👍](docs/src/sidebar/sanfene/shejimoshi.md) -- [面渣逆袭(Linux面试题八股文)必看👍](docs/src/sidebar/sanfene/linux.md) +# Java核心 -# Java基础 +> [!NOTE] +> **Java核心非常重要**!我将其分成了Java 基础篇(包括基础语法、面向对象、集合框架、异常处理、Java IO 等)、Java 并发篇和 Java 虚拟机篇。 -> **Java基础非常重要**!包括基础语法、面向对象、集合框架、异常处理、Java IO、网络编程、NIO、并发编程和 JVM。 +## Java概述 -## Java概述及环境配置 +- [什么是Java?Java发展简史,Java的优势](docs/overview/what-is-java.md) +- [第一个Java程序:Hello World](docs/overview/hello-world.md) -- [《二哥的Java进阶之路》小册简介](docs/src/overview/readme.md) -- [Java简史、特性、前景](docs/src/overview/what-is-java.md) -- [Windows和macOS下安装JDK教程](docs/src/overview/jdk-install-config.md) -- [在macOS和Windows上安装Intellij IDEA](docs/src/overview/IDEA-install-config.md) -- [编写第一个程序Hello World](docs/src/overview/hello-world.md) ## Java基础语法 -- [48个关键字及2个保留字全解析](docs/src/basic-extra-meal/48-keywords.md) -- [了解Java注释](docs/src/basic-grammar/javadoc.md) -- [基本数据类型与引用数据类型](docs/src/basic-grammar/basic-data-type.md) -- [自动类型转换与强制类型转换](docs/src/basic-grammar/type-cast.md) -- [Java基本数据类型缓存池剖析(IntegerCache)](docs/src/basic-extra-meal/int-cache.md) -- [Java运算符详解](docs/src/basic-grammar/operator.md) -- [Java流程控制语句详解](docs/src/basic-grammar/flow-control.md) -- [Java 语法基础练习题](docs/src/basic-grammar/basic-exercise.md) - -## 数组&字符串 - -- [掌握Java数组](docs/src/array/array.md) -- [掌握 Java二维数组](docs/src/array/double-array.md) -- [如何优雅地打印Java数组?](docs/src/array/print.md) -- [深入解读String类源码](docs/src/string/string-source.md) -- [为什么Java字符串是不可变的?](docs/src/string/immutable.md) -- [深入理解Java字符串常量池](docs/src/string/constant-pool.md) -- [详解 String.intern() 方法](docs/src/string/intern.md) -- [String、StringBuilder、StringBuffer](docs/src/string/builder-buffer.md) -- [Java中equals()与==的区别](docs/src/string/equals.md) -- [最优雅的Java字符串拼接是哪种方式?](docs/src/string/join.md) -- [如何在Java中拆分字符串?](docs/src/string/split.md) +- [Java支持的8种基本数据类型](docs/basic-grammar/basic-data-type.md) +- [Java流程控制语句](docs/basic-grammar/flow-control.md) +- [Java运算符](docs/basic-grammar/operator.md) +- [Java注释:单行、多行和文档注释](docs/basic-grammar/javadoc.md) +- [Java中常用的48个关键字](docs/basic-extra-meal/48-keywords.md) +- [Java命名规范(非常全面,可以收藏)](docs/basic-extra-meal/java-naming.md) ## Java面向对象编程 -- [类和对象](docs/src/oo/object-class.md) -- [Java中的包](docs/src/oo/package.md) -- [Java变量](docs/src/oo/var.md) -- [Java方法](docs/src/oo/method.md) -- [Java可变参数详解](docs/src/basic-extra-meal/varables.md) -- [手把手教你用 C语言实现 Java native 本地方法](docs/src/oo/native-method.md) -- [Java构造方法](docs/src/oo/construct.md) -- [Java访问权限修饰符](docs/src/oo/access-control.md) -- [Java代码初始化块](docs/src/oo/code-init.md) -- [Java抽象类](docs/src/oo/abstract.md) -- [Java接口](docs/src/oo/interface.md) -- [Java内部类](docs/src/oo/inner-class.md) -- [深入理解Java三大特性:封装、继承和多态](docs/src/oo/encapsulation-inheritance-polymorphism.md) -- [详解Java this与super关键字](docs/src/oo/this-super.md) -- [详解Java static 关键字](docs/src/oo/static.md) -- [详解Java final 关键字](docs/src/oo/final.md) -- [掌握Java instanceof关键字](docs/src/basic-extra-meal/instanceof.md) -- [聊聊Java中的不可变对象](docs/src/basic-extra-meal/immutable.md) -- [方法重写 Override 和方法重载 Overload 有什么区别?](docs/src/basic-extra-meal/override-overload.md) -- [深入理解Java中的注解](docs/src/basic-extra-meal/annotation.md) -- [Java枚举:小小enum,优雅而干净](docs/src/basic-extra-meal/enum.md) +- [怎么理解Java中类和对象的概念?](docs/oo/object-class.md) +- [Java变量的作用域:局部变量、成员变量、静态变量、常量](docs/oo/var.md) +- [Java方法](docs/oo/method.md) +- [Java构造方法](docs/oo/construct.md) +- [Java代码初始化块](docs/oo/code-init.md) +- [Java抽象类](docs/oo/abstract.md) +- [Java接口](docs/oo/interface.md) +- [Java中的static关键字解析](docs/oo/static.md) +- [Java中this和super的用法总结](docs/oo/this-super.md) +- [浅析Java中的final关键字](docs/oo/final.md) +- [Java instanceof关键字用法](docs/oo/instanceof.md) +- [深入理解Java中的不可变对象](docs/basic-extra-meal/immutable.md) +- [Java中可变参数的使用](docs/basic-extra-meal/varables.md) +- [深入理解Java泛型](docs/basic-extra-meal/generic.md) +- [深入理解Java注解](docs/basic-extra-meal/annotation.md) +- [Java枚举(enum)](docs/basic-extra-meal/enum.md) +- [大白话说Java反射:入门、使用、原理](docs/basic-extra-meal/fanshe.md) + +## 字符串&数组 + +- [为什么String是不可变的?](docs/string/immutable.md) +- [深入了解Java字符串常量池](docs/string/constant-pool.md) +- [深入解析 String#intern](docs/string/intern.md) +- [Java判断两个字符串是否相等?](docs/string/equals.md) +- [Java字符串拼接的几种方式](docs/string/join.md) +- [如何在Java中优雅地分割String字符串?](docs/string/split.md) +- [深入理解Java数组](docs/array/array.md) +- [如何优雅地打印Java数组?](docs/array/print.md) ## 集合框架(容器) -- [Java集合框架概览,包括List、Set、Map、队列](docs/src/collection/gailan.md) -- [深入探讨 Java ArrayList](docs/src/collection/arraylist.md) -- [深入探讨 Java LinkedList](docs/src/collection/linkedlist.md) -- [Java Stack详解](docs/src/collection/stack.md) -- [Java HashMap详解](docs/src/collection/hashmap.md) -- [Java LinkedHashMap详解](docs/src/collection/linkedhashmap.md) -- [Java TreeMap详解](docs/src/collection/treemap.md) -- [Java 双端队列 ArrayDeque详解](docs/src/collection/arraydeque.md) -- [Java 优先级队列PriorityQueue详解](docs/src/collection/PriorityQueue.md) -- [Java Comparable和Comparator的区别](docs/src/collection/comparable-omparator.md) -- [时间复杂度,评估ArrayList和LinkedList的执行效率](docs/src/collection/time-complexity.md) -- [ArrayList和LinkedList的区别](docs/src/collection/list-war-2.md) -- [Java 泛型深入解析](docs/src/basic-extra-meal/generic.md) -- [Java迭代器Iterator和Iterable有什么区别?](docs/src/collection/iterator-iterable.md) -- [为什么禁止在foreach里执行元素的删除操作?](docs/src/collection/fail-fast.md) - -## Java IO - -- [深入了解 Java IO](docs/src/io/shangtou.md) -- [Java File:IO 流的起点与终点](docs/src/io/file-path.md) -- [Java 字节流:Java IO 的基石](docs/src/io/stream.md) -- [Java 字符流:Reader和Writer的故事](docs/src/io/reader-writer.md) -- [Java 缓冲流:Java IO 的读写效率有了质的飞升](docs/src/io/buffer.md) -- [Java 转换流:Java 字节流和字符流的桥梁](docs/src/io/char-byte.md) -- [Java 打印流:PrintStream & PrintWriter](docs/src/io/print.md) -- [Java 序列流:Java 对象的序列化和反序列化](docs/src/io/serialize.md) -- [Java Serializable 接口:明明就一个空的接口嘛](docs/src/io/Serializbale.md) -- [深入探讨 Java transient 关键字](docs/src/io/transient.md) +- [Java集合框架](docs/collection/gailan.md) +- [Java集合ArrayList详解](docs/collection/arraylist.md) +- [Java集合LinkedList详解](docs/collection/linkedlist.md) +- [Java中ArrayList和LinkedList的区别](docs/collection/list-war-2.md) +- [Java中的Iterator和Iterable区别](docs/collection/iterator-iterable.md) +- [为什么阿里巴巴强制不要在foreach里执行删除操作](docs/collection/fail-fast.md) +- [Java8系列之重新认识HashMap](docs/collection/hashmap.md) -## 异常处理 - -- [一文彻底搞懂Java异常处理,YYDS](docs/src/exception/gailan.md) -- [深入理解 Java 中的 try-with-resources](docs/src/exception/try-with-resources.md) -- [Java异常处理的20个最佳实践](docs/src/exception/shijian.md) -- [空指针NullPointerException的传说](docs/src/exception/npe.md) -- [try-catch 捕获异常真的会影响性能吗?](docs/src/exception/try-catch-xingneng.md) +## Java输入输出 -## 常用工具类 +- [Java IO学习整理](docs/io/shangtou.md) +- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](docs/io/BIONIOAIO.md) -- [Java Scanner:扫描控制台输入的工具类](docs/src/common-tool/scanner.md) -- [Java Arrays:专为数组而生的工具类](docs/src/common-tool/arrays.md) -- [Apache StringUtils:专为Java字符串而生的工具类](docs/src/common-tool/StringUtils.md) -- [Objects:专为操作Java对象而生的工具类](docs/src/common-tool/Objects.md) -- [Java Collections:专为集合而生的工具类](docs/src/common-tool/collections.md) -- [Hutool:国产良心工具包,让你的Java变得更甜](docs/src/common-tool/hutool.md) -- [Guava:Google开源的Java工具库,太强大了](docs/src/common-tool/guava.md) -- [其他常用Java工具类:IpUtil、MDC、ClassUtils、BeanUtils、ReflectionUtils](docs/src/common-tool/utils.md) -## Java新特性 - -- [Java 8 Stream流:掌握流式编程的精髓](docs/src/java8/stream.md) -- [Java 8 Optional最佳指南:解决空指针问题的优雅之选](docs/src/java8/optional.md) -- [深入浅出Java 8 Lambda表达式:探索函数式编程的魅力](docs/src/java8/Lambda.md) -- [Java 14 开箱,新特性Record、instanceof、switch香香香香](docs/src/java8/java14.md) +## 异常处理 -## Java网络编程 +- [一文读懂Java异常处理](docs/exception/gailan.md) +- [详解Java7新增的try-with-resouces语法](docs/exception/try-with-resouces.md) +- [Java异常处理的20个最佳实践](docs/exception/shijian.md) +- [Java空指针NullPointerException的传说](docs/exception/npe.md) -- [Java网络编程的基础:计算机网络](docs/src/socket/network-base.md) -- [Java Socket:飞鸽传书的网络套接字](docs/src/socket/socket.md) -- [牛逼,用Java Socket手撸了一个HTTP服务器](docs/src/socket/http.md) +## 常用工具类 -## Java NIO +- [Java Arrays工具类10大常用方法](docs/common-tool/arrays.md) +- [Java集合框架:Collections工具类](docs/common-tool/collections.md) +- [Hutool:国产良心工具包,让你的Java变得更甜](docs/common-tool/hutool.md) +- [Google开源的Guava工具库,太强大了~](docs/common-tool/guava.md) -- [Java NIO 比传统 IO 强在哪里?](docs/src/nio/nio-better-io.md) -- [一文彻底解释清楚Java 中的NIO、BIO和AIO](docs/src/nio/BIONIOAIO.md) -- [详解Java NIO的Buffer缓冲区和Channel通道](docs/src/nio/buffer-channel.md) -- [聊聊 Java NIO中的Paths、Files](docs/src/nio/paths-files.md) -- [Java NIO 网络编程实践:从入门到精通](docs/src/nio/network-connect.md) -- [一文彻底理解Java IO模型](docs/src/nio/moxing.md) +## Java新特性 -## 重要知识点 +- [Java 8 Stream流详细用法](docs/java8/stream.md) +- [Java 8 Optional最佳指南](docs/java8/optional.md) +- [深入浅出Java 8 Lambda表达式](docs/java8/Lambda.md) + +## Java重要知识点 + +- [彻底弄懂Java中的Unicode和UTF-8编码](docs/basic-extra-meal/java-unicode.md) +- [Java中int、Integer、new Integer之间的区别](docs/basic-extra-meal/int-cache.md) +- [深入剖析Java中的拆箱和装箱](docs/basic-extra-meal/box.md) +- [彻底讲明白的Java浅拷贝与深拷贝](docs/basic-extra-meal/deep-copy.md) +- [深入理解Java中的hashCode方法](docs/basic-extra-meal/hashcode.md) +- [一次性搞清楚equals和hashCode](docs/basic-extra-meal/equals-hashcode.md) +- [Java重写(Override)与重载(Overload)](docs/basic-extra-meal/override-overload.md) +- [Java重写(Overriding)时应当遵守的11条规则](docs/basic-extra-meal/Overriding.md) +- [Java到底是值传递还是引用传递?](docs/basic-extra-meal/pass-by-value.md) +- [Java不能实现真正泛型的原因是什么?](docs/basic-extra-meal/true-generic.md) +- [详解Java中Comparable和Comparator的区别](docs/basic-extra-meal/comparable-omparator.md) +- [jdk9为何要将String的底层实现由char[]改成了byte[]?](docs/basic-extra-meal/jdk9-char-byte-string.md) +- [为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)?](docs/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md) +- [先有Class还是先有Object?](docs/basic-extra-meal/class-object.md) +- [instanceof关键字是如何实现的?](docs/basic-extra-meal/instanceof-jvm.md) -- [Java命名规范:编写可读性强的代码](docs/src/basic-extra-meal/java-naming.md) -- [解决中文乱码:字符编码全攻略 - ASCII、Unicode、UTF-8、GB2312详解](docs/src/basic-extra-meal/java-unicode.md) -- [深入浅出Java拆箱与装箱](docs/src/basic-extra-meal/box.md) -- [深入理解Java浅拷贝与深拷贝](docs/src/basic-extra-meal/deep-copy.md) -- [Java hashCode方法解析](docs/src/basic-extra-meal/hashcode.md) -- [Java到底是值传递还是引用传递?](docs/src/basic-extra-meal/pass-by-value.md) -- [为什么无法实现真正的泛型?](docs/src/basic-extra-meal/true-generic.md) -- [Java 反射详解](docs/src/basic-extra-meal/fanshe.md) ## Java并发编程 -- [并发编程小册简介](docs/src/thread/readme.md) -- [Java多线程入门](docs/src/thread/wangzhe-thread.md) -- [获取线程的执行结果](docs/src/thread/callable-future-futuretask.md) -- [Java线程的6种状态及切换](docs/src/thread/thread-state-and-method.md) -- [线程组和线程优先级](docs/src/thread/thread-group-and-thread-priority.md) -- [进程与线程的区别](docs/src/thread/why-need-thread.md) -- [多线程带来了哪些问题?](docs/src/thread/thread-bring-some-problem.md) -- [Java的内存模型(JMM)](docs/src/thread/jmm.md) -- [volatile关键字解析](docs/src/thread/volatile.md) -- [synchronized关键字解析](docs/src/thread/synchronized-1.md) -- [synchronized的四种锁状态](docs/src/thread/synchronized.md) -- [深入浅出偏向锁](docs/src/thread/pianxiangsuo.md) -- [CAS详解](docs/src/thread/cas.md) -- [AQS详解](docs/src/thread/aqs.md) -- [锁分类和 JUC](docs/src/thread/lock.md) -- [重入锁ReentrantLock](docs/src/thread/reentrantLock.md) -- [读写锁ReentrantReadWriteLock](docs/src/thread/ReentrantReadWriteLock.md) -- [等待通知条件Condition](docs/src/thread/condition.md) -- [线程阻塞唤醒类LockSupport](docs/src/thread/LockSupport.md) -- [Java的并发容器](docs/src/thread/map.md) -- [并发容器ConcurrentHashMap](docs/src/thread/ConcurrentHashMap.md) -- [非阻塞队列ConcurrentLinkedQueue](docs/src/thread/ConcurrentLinkedQueue.md) -- [阻塞队列BlockingQueue](docs/src/thread/BlockingQueue.md) -- [并发容器CopyOnWriteArrayList](docs/src/thread/CopyOnWriteArrayList.md) -- [本地变量ThreadLocal](docs/src/thread/ThreadLocal.md) -- [线程池](docs/src/thread/pool.md) -- [定时任务ScheduledThreadPoolExecutor](docs/src/thread/ScheduledThreadPoolExecutor.md) -- [原子操作类Atomic](docs/src/thread/atomic.md) -- [魔法类 Unsafe](docs/src/thread/Unsafe.md) -- [通信工具类](docs/src/thread/CountDownLatch.md) -- [Fork/Join](docs/src/thread/fork-join.md) -- [生产者-消费者模式](docs/src/thread/shengchanzhe-xiaofeizhe.md) +- [室友打了一把王者就学会了创建Java线程的3种方式](docs/thread/wangzhe-thread.md) +- [Java线程的6种状态及切换(透彻讲解)](docs/thread/thread-state-and-method.md) +- [线程组是什么?线程优先级如何设置?](docs/thread/thread-group-and-thread-priority.md) +- [进程与线程的区别是什么?](docs/thread/why-need-thread.md) +- [并发编程带来了哪些问题?](docs/thread/thread-bring-some-problem.md) +- [全面理解Java的内存模型(JMM)](docs/thread/jmm.md) +- [Java并发编程volatile关键字解析](docs/thread/volatile.md) +- [Java中的synchronized关键字锁的到底是什么?](docs/thread/synchronized.md) +- [Java实现CAS的原理](docs/thread/cas.md) +- [Java并发AQS详解](docs/thread/aqs.md) +- [大致了解下Java的锁接口和锁](docs/thread/lock.md) +- [深入理解Java并发重入锁ReentrantLock](docs/thread/reentrantLock.md) +- [深入理解Java并发读写锁ReentrantReadWriteLock](docs/thread/ReentrantReadWriteLock.md) +- [深入理解Java并发线程协作类Condition](docs/thread/condition.md) +- [深入理解Java并发线程线程阻塞唤醒类LockSupport](docs/thread/LockSupport.md) +- [简单聊聊Java的并发集合容器](docs/thread/map.md) +- [吊打Java并发面试官之ConcurrentHashMap](docs/thread/ConcurrentHashMap.md) +- [吊打Java并发面试官之ConcurrentLinkedQueue](docs/thread/ConcurrentLinkedQueue.md) +- [吊打Java并发面试官之CopyOnWriteArrayList](docs/thread/CopyOnWriteArrayList.md) +- [吊打Java并发面试官之ThreadLocal](docs/thread/ThreadLocal.md) +- [吊打Java并发面试官之BlockingQueue](docs/thread/BlockingQueue.md) +- [面试必备:Java线程池解析](docs/thread/pool.md) +- [为什么阿里巴巴要禁用Executors创建线程池?](docs/thread/ali-executors.md) +- [深入剖析Java计划任务ScheduledThreadPoolExecutor](docs/thread/ScheduledThreadPoolExecutor.md) +- [Java atomic包中的原子操作类总结](docs/thread/atomic.md) +- [Java并发编程通信工具类CountDownLatch等一网打尽](docs/thread/CountDownLatch.md) +- [深入理解Java并发编程之Fork/Join框架](docs/thread/fork-join.md) +- [从根上理解生产者-消费者模式](docs/thread/shengchanzhe-xiaofeizhe.md) ## Java虚拟机 -- [JVM小册简介](docs/src/jvm/readme.md) -- [大白话带你认识JVM](docs/src/jvm/what-is-jvm.md) -- [JVM是如何运行Java代码的?](docs/src/jvm/how-run-java-code.md) -- [Java的类加载机制(付费)](docs/src/jvm/class-load.md) -- [Java的类文件结构](docs/src/jvm/class-file-jiegou.md) -- [从javap的角度轻松看懂字节码](docs/src/jvm/bytecode.md) -- [栈虚拟机与寄存器虚拟机](docs/src/jvm/vm-stack-register.md) -- [字节码指令详解](docs/src/jvm/zijiema-zhiling.md) -- [深入理解JVM的栈帧结构](docs/src/jvm/stack-frame.md) -- [深入理解JVM的运行时数据区](docs/src/jvm/neicun-jiegou.md) -- [深入理解JVM的垃圾回收机制](docs/src/jvm/gc.md) -- [深入理解 JVM 的垃圾收集器:CMS、G1、ZGC](docs/src/jvm/gc-collector.md) -- [Java 创建的对象到底放在哪?](docs/src/jvm/whereis-the-object.md) -- [深入理解JIT(即时编译)](docs/src/jvm/jit.md) -- [JVM 性能监控之命令行篇](docs/src/jvm/console-tools.md) -- [JVM 性能监控之可视化篇](docs/src/jvm/view-tools.md) -- [阿里开源的 Java 诊断神器 Arthas](docs/src/jvm/arthas.md) -- [内存溢出排查优化实战](docs/src/jvm/oom.md) -- [CPU 100% 排查优化实践](docs/src/jvm/cpu-percent-100.md) -- [JVM 核心知识点总结](docs/src/jvm/zongjie.md) - - -# Java进阶 - -> - **到底能不能成为一名合格的 Java 程序员,从理论走向实战?Java进阶这部分内容就是一个分水岭**! +- [JVM到底是什么?](docs/jvm/what-is-jvm.md) +- [JVM到底是如何运行Java代码的?](docs/jvm/how-run-java-code.md) +- [我竟然不再抗拒Java的类加载机制了](docs/jvm/class-load.md) +- [详解Java的类文件(class文件)结构](docs/jvm/class-file-jiegou.md) +- [从javap的角度轻松看懂字节码](docs/jvm/bytecode.md) +- [JVM字节码指令详解](docs/jvm/zijiema-zhiling.md) +- [虚拟机是如何执行字节码指令的?](docs/jvm/how-jvm-run-zijiema-zhiling.md) +- [HSDB(Hotspot Debugger)从入门到实战](docs/jvm/hsdb.md) +- [史上最通俗易懂的ASM教程](docs/jvm/asm.md) +- [自己编译JDK](docs/jvm/compile-jdk.md) +- [深入理解JVM的内存结构](docs/jvm/neicun-jiegou.md) +- [Java 创建的对象到底放在哪?](docs/jvm/whereis-the-object.md) +- [咱们从头到尾说一次Java垃圾回收](docs/jvm/gc.md) +- [图解Java的垃圾回收机制](docs/jvm/tujie-gc.md) +- [Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析)](docs/jvm/problem-tools.md) +- [Java即时编译(JIT)器原理解析及实践](docs/jvm/jit.md) +- [一次内存溢出排查优化实战](docs/jvm/oom.md) +- [一次生产CPU 100% 排查优化实践](docs/jvm/cpu-percent-100.md) +- [JVM 核心知识点总结](docs/jvm/zongjie.md) + + +# Java企业级开发 + +> [!NOTE] +> - **到底能不能成为一名合格的 Java 程序员,从理论走向实战?Java 企业级开发这部分内容就是一个分水岭**! > - 纸上得来终觉浅,须知此事要躬行。 -## 开发/构建工具 - -> 工欲善其事必先利其器,这句话大家都耳熟能详了,熟练使用开发/构建工具可以让我们极大提升开发效率,解放生产力。 - -- [5分钟带你深入浅出搞懂Nginx](docs/src/nginx/nginx.md) - -### IDEA - -> 集成开发环境,Java 党主要就是 Intellij IDEA 了,号称史上最强大的 Java 开发工具,没有之一。 - -- [分享 4 个阅读源码必备的 IDEA 调试技巧](docs/src/ide/4-debug-skill.md) -- [分享 1 个可以在 IDEA 里下五子棋的插件](docs/src/ide/xechat.md) -- [分享 10 个可以一站式开发的 IDEA 神级插件](docs/src/ide/shenji-chajian-10.md) +## 开发工具 -### Maven +- [终于把项目构建神器Maven捋清楚了~](docs/maven/maven.md) +- [我在工作中是如何使用Git的](docs/git/git-qiyuan.md) +- [5分钟带你深入浅出搞懂Nginx](docs/nginx/nginx.md) -> Maven 是目前比较流行的一个项目构建工具,基于 pom 坐标来帮助我们管理第三方依赖,以及项目打包。 +## IDE/编辑器 -- [终于把项目构建神器Maven捋清楚了~](docs/src/maven/maven.md) - -### Git - -> Git 是一个分布式版本控制系统,缔造者是大名鼎鼎的林纳斯·托瓦茲 (Linus Torvalds),Git 最初的目的是为了能更好的管理 Linux 内核源码。如今,Git 已经成为全球软件开发者的标配。如果说 Linux 项目促成了开源软件的成功并改写了软件行业的格局,那么 Git 则是改变了全世界开发者的工作方式和写作方式。 - -- [1小时彻底掌握Git](docs/src/git/git-qiyuan.md) -- [GitHub 远程仓库端口切换](docs/src/git/port-22-to-443.md) +- [4个高级的IntelliJ IDEA调试技巧](docs/ide/4-debug-skill.md) ## Spring -- [Spring AOP扫盲](docs/src/springboot/aop-log.md) -- [Spring IoC扫盲](docs/src/springboot/ioc.md) +- [Spring AOP扫盲](docs/springboot/aop-log.md) +- [Spring IoC扫盲](docs/springboot/ioc.md) ## SpringBoot -- [一分钟快速搭建Spring Boot项目](docs/src/springboot/initializr.md) -- [Spring Boot 整合 lombok](docs/src/springboot/lombok.md) -- [Spring Boot 整合 MySQL 和 Druid](docs/src/springboot/mysql-druid.md) -- [Spring Boot 整合 JPA](docs/src/springboot/jpa.md) -- [Spring Boot 整合 Thymeleaf 模板引擎](docs/src/springboot/thymeleaf.md) -- [Spring Boot 如何开启事务支持?](docs/src/springboot/transaction.md) -- [Spring Boot 中使用过滤器、拦截器、监听器](docs/src/springboot/Filter-Interceptor-Listener.md) -- [Spring Boot 整合 Redis 实现缓存](docs/src/redis/redis-springboot.md) -- [Spring Boot 整合 Logback 定制日志框架](docs/src/springboot/logback.md) -- [Spring Boot 整合 Swagger-UI 实现在线API文档](docs/src/springboot/swagger.md) -- [Spring Boot 整合 Knife4j,美化强化丑陋的Swagger](docs/src/gongju/knife4j.md) -- [Spring Boot 整合 Spring Task 实现定时任务](docs/src/springboot/springtask.md) -- [Spring Boot 整合 MyBatis-Plus AutoGenerator 生成编程喵项目骨架代码](docs/src/kaiyuan/auto-generator.md) -- [Spring Boot 整合Quartz实现编程喵定时发布文章](docs/src/springboot/quartz.md) -- [Spring Boot 整合 MyBatis](docs/src/springboot/mybatis.md) -- [一键部署 Spring Boot 到远程 Docker 容器](docs/src/springboot/docker.md) -- [如何在本地(macOS环境)跑起来编程喵(Spring Boot+Vue)项目源码?](docs/src/springboot/macos-codingmore-run.md) -- [如何在本地(Windows环境)跑起来编程喵(Spring Boot+Vue)项目源码?](docs/src/springboot/windows-codingmore-run.md) -- [编程喵🐱实战项目如何在云服务器上跑起来?](docs/src/springboot/linux-codingmore-run.md) -- [SpringBoot中处理校验逻辑的两种方式:Hibernate Validator+全局异常处理](docs/src/springboot/validator.md) - - -## Netty - -- [超详细Netty入门,看这篇就够了!](docs/src/netty/rumen.md) - - -## 辅助工具 - -- [Chocolatey:一款GitHub星标8.2k+的Windows命令行软件管理器,好用到爆!](docs/src/gongju/choco.md) -- [Homebrew,GitHub 星标 32.5k+的 macOS 命令行软件管理神器,功能真心强大!](docs/src/gongju/brew.md) -- [Tabby:一款逼格更高的开源终端工具,GitHub 星标 21.4k](docs/src/gongju/tabby.md) -- [Warp:号称下一代终端神器,GitHub星标2.8k+,用完爱不释手](docs/src/gongju/warp.md) -- [WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了!](docs/src/gongju/windterm.md) -- [chiner:干掉 PowerDesigner,国人开源的数据库设计工具,界面漂亮,功能强大](docs/src/gongju/chiner.md) -- [DBeaver:干掉付费的 Navicat,操作所有数据库就靠它了!](docs/src/gongju/DBeaver.md) - -## 开源轮子 - -- [Forest:一款极简的声明式HTTP调用API框架](docs/src/gongju/forest.md) -- [Junit:一个开源的Java单元测试框架](docs/src/gongju/junit.md) -- [fastjson:阿里巴巴开源的JSON解析库](docs/src/gongju/fastjson.md) -- [Gson:Google开源的JSON解析库](docs/src/gongju/gson.md) -- [Jackson:GitHub上star数最多的JSON解析库](docs/src/gongju/jackson.md) -- [Log4j:Java日志框架的鼻祖](docs/src/gongju/log4j.md) -- [Log4j 2:Apache维护的一款高性能日志记录工具](docs/src/gongju/log4j2.md) -- [Logback:Spring Boot内置的日志处理框架](docs/src/gongju/logback.md) -- [SLF4J:阿里巴巴强制使用的日志门面担当](docs/src/gongju/slf4j.md) - +- [一分钟快速搭建Spring Boot项目](docs/springboot/initializr.md) +- [Spring Boot 整合 MySQL 和 Druid](docs/springboot/mysql-druid.md) +- [Spring Boot 整合 JPA](docs/springboot/jpa.md) +- [Spring Boot 整合 Thymeleaf 模板引擎](docs/springboot/thymeleaf.md) +- [Spring Boot 如何开启事务支持?](docs/springboot/transaction.md) +- [Spring Boot 中使用过滤器、拦截器、监听器](docs/springboot/Filter-Interceptor-Listener.md) +- [Spring Boot 整合 Redis 实现缓存](docs/redis/redis-springboot.md) +- [Spring Boot 整合 Logback 定制日志框架](docs/springboot/logback.md) +- [Spring Boot 整合 Swagger-UI 实现在线API文档](docs/springboot/swagger.md) +- [Spring Boot 整合 Knife4j,美化强化丑陋的Swagger](docs/gongju/knife4j.md) +- [Spring Boot 整合 Spring Task 实现定时任务](docs/springboot/springtask.md) +- [Spring Boot 整合 MyBatis-Plus AutoGenerator 生成编程喵项目骨架代码](docs/kaiyuan/auto-generator.md) + + +## 辅助工具/轮子 + +- [Tabby:一款逼格更高的开源终端工具](docs/gongju/tabby.md) +- [Warp:一款21世纪人用的终端工具](docs/gongju/warp.md) +- [Chocolatey:一款GitHub星标8.2k+的Windows命令行软件管理器](docs/gongju/choco.md) +- [chiner:一款开源的数据库设计神器](docs/gongju/chiner.md) +- [DBeaver:一款免费的数据库操作工具](docs/gongju/DBeaver.md) +- [Forest:一款极简的声明式HTTP调用API框架](docs/gongju/forest.md) +- [Junit:一个开源的Java单元测试框架](docs/gongju/junit.md) +- [fastjson:阿里巴巴开源的JSON解析库](docs/gongju/fastjson.md) +- [Gson:Google开源的JSON解析库](docs/gongju/gson.md) +- [Jackson:GitHub上star数最多的JSON解析库](docs/gongju/jackson.md) +- [Log4j:Java日志框架的鼻祖](docs/gongju/log4j.md) +- [Log4j 2:Apache维护的一款高性能日志记录工具](docs/gongju/log4j2.md) +- [Logback:Spring Boot内置的日志处理框架](docs/gongju/logback.md) +- [SLF4J:阿里巴巴强制使用的日志门面担当](docs/gongju/slf4j.md) + + +## 安全篇 ## 分布式 -- [全文搜索引擎Elasticsearch入门教程](docs/src/elasticsearch/rumen.md) -- [可能是把ZooKeeper概念讲的最清楚的一篇文章](docs/src/zookeeper/jibenjieshao.md) -- [微服务网关:从对比到选型,由理论到实践](docs/src/microservice/api-wangguan.md) +- [全文搜索引擎Elasticsearch入门教程](docs/elasticsearch/rumen.md) +- [可能是把ZooKeeper概念讲的最清楚的一篇文章](docs/zookeeper/jibenjieshao.md) +- [微服务网关:从对比到选型,由理论到实践](docs/microservice/api-wangguan.md) -## 消息队列 +## 高性能 -- [RabbitMQ入门教程(概念、应用场景、安装、使用)](docs/src/mq/rabbitmq-rumen.md) -- [怎么确保消息100%不丢失?](docs/src/mq/100-budiushi.md) -- [Kafka核心知识点大梳理](docs/src/mq/kafka.md) +### 消息队列 + +- [RabbitMQ入门教程(概念、应用场景、安装、使用)](docs/mq/rabbitmq-rumen.md) +- [怎么确保消息100%不丢失?](docs/mq/100-budiushi.md) + +## 高可用 # 数据库 +> [!NOTE] > - **简而言之,就是按照数据结构来组织、存储和管理数据的仓库**。几乎所有的 Java 后端开发都要学习数据库这块的知识,包括关系型数据库 MySQL,缓存中间件 Redis,非关系型数据库 MongoDB 等。 ## MySQL -- [MySQL 的安装和连接,结合技术派实战项目来讲](docs/src/mysql/install.md) -- [MySQL 的数据库操作,利用 Spring Boot 实现数据库的自动创建](docs/src/mysql/database.md) -- [MySQL 表的基本操作,结合技术派的表自动初始化来讲](docs/src/mysql/table.md) -- [MySQL 的数据类型,4000 字 20 张手绘图,彻底掌握](docs/src/mysql/data-type.md) -- [MySQL 的字符集和比较规则,从跟上掌握](docs/src/mysql/charset.md) -- [MySQL bin目录下的那些可执行文件,包括备份数据库、导入 CSV 等](docs/src/mysql/bin.md) -- [MySQL 的字段属性,默认值、是否为空、主键、自增、ZEROLFILL等一网打尽](docs/src/mysql/column.md) -- [MySQL 的简单查询,开始踏上 SELECT 之旅](docs/src/mysql/select-simple.md) -- [MySQL 的 WEHRE 条件查询,重点搞懂 % 通配符](docs/src/mysql/select-where.md) -- [如何保障MySQL和Redis的数据一致性?](docs/src/mysql/redis-shuju-yizhixing.md) -- [从根上理解 MySQL 的事务](docs/src/mysql/lijie-shiwu.md) -- [浅入深出 MySQL 中事务的实现](docs/src/mysql/shiwu-shixian.md) +- [如何保障MySQL和Redis的数据一致性?](docs/mysql/redis-shuju-yizhixing.md) ## Redis -- [Redis入门(适合新手)](docs/src/redis/rumen.md) -- [聊聊缓存雪崩、穿透、击穿](docs/src/redis/xuebeng-chuantou-jichuan.md) +- [Redis入门(适合新手)](docs/redis/rumen.md) +- [聊聊缓存雪崩、穿透、击穿](docs/redis/xuebeng-chuantou-jichuan.md) ## MongoDB -- [MongoDB最基础入门教程](docs/src/mongodb/rumen.md) +- [MongoDB最基础入门教程](docs/mongodb/rumen.md) # 计算机基础 +> [!NOTE] > - **计算机基础包括操作系统、计算机网络、计算机组成原理、数据结构与算法等**。对于任何一名想要走得更远的 Java 后端开发来说,都是必须要花时间和精力去夯实的。 > - 万丈高露平地起,勿在浮沙筑高台。 -- [操作系统核心知识点大梳理](docs/src/cs/os.md) -- [计算机网络核心知识点大梳理](docs/src/cs/wangluo.md) +- [计算机操作系统知识点大梳理](docs/cs/os.md) +- [计算机网络核心知识点大梳理](docs/cs/wangluo.md) # 求职面试 +> [!NOTE] > - **学习了那么多 Java 知识,耗费了无数的脑细胞,熬掉了无数根秀发,为的是什么?当然是谋取一份心仪的 offer 了**。那八股文、面试题、城市选择、优质面经又怎能少得了呢? > - 千淘万漉虽辛苦,吹尽狂沙始到金。 -## 面试题&八股文 +## 面试题集合 -- [34 道 Java 精选面试题👍](docs/src/interview/java-34.md) -- [13 道 Java HashMap 精选面试题👍](docs/src/interview/java-hashmap-13.md) -- [60 道 MySQL 精选面试题👍](docs/src/interview/mysql-60.md) -- [15 道 MySQL 索引精选面试题👍](docs/src/interview/mysql-suoyin-15.md) -- [12 道 Redis 精选面试题👍](docs/src/interview/redis-12.md) -- [40 道 Nginx 精选面试题👍](docs/src/interview/nginx-40.md) -- [17 道 Dubbo 精选面试题👍](docs/src/interview/dubbo-17.md) -- [40 道 Kafka 精选面试题👍](docs/src/interview/kafka-40.md) -- [Java 基础背诵版八股文必看🍉](docs/src/interview/java-basic-baguwen.md) -- [Java 并发编程背诵版八股文必看🍉](docs/src/interview/java-thread-baguwen.md) -- [Java 虚拟机背诵版八股文必看🍉](docs/src/interview/java-jvm-baguwen.md) -- [携程面试官👤:大文件上传时如何做到秒传?](docs/src/interview/mianshiguan-bigfile-miaochuan.md) -- [阿里面试官👤:为什么要分库分表?](docs/src/interview/mianshiguan-fenkufenbiao.md) -- [淘宝面试官👤:优惠券系统该如何设计?](docs/src/interview/mianshiguan-youhuiquan.md) +- [Java:34道精选高频面试题](docs/baguwen/java-basic-34.md) +- [Java:13道HashMap精选面试题](docs/collection/hashmap-interview.md) +- [Redis:12道精选高频面试题)](docs/mianjing/redis12question.md) +- [Nginx:40道精选面试题](docs/nginx/40-interview.md) -## 优质面经 +## 背诵版八股文 -- [硕士读者春招斩获深圳腾讯PCG和杭州阿里云 offer✌️](docs/src/mianjing/shanganaliyun.md) -- [本科读者小公司一年工作经验社招拿下阿里美团头条京东滴滴等 offer✌️](docs/src/mianjing/shezynmjfxhelmtttjddd.md) -- [非科班读者,用一年时间社招拿下阿里 Offer✌️](docs/src/mianjing/xuelybdzheloffer.md) -- [二本读者社招两年半10家公司28轮面试面经✌️](docs/src/mianjing/huanxgzl.md) -- [双非一本秋招收获腾讯ieg、百度、字节等6家大厂offer✌️](docs/src/mianjing/quzjlsspdx.md) -- [双非学弟收割阿里、字节、B站校招 offer,附大学四年硬核经验总结✌️](docs/src/mianjing/zheisnylzldhzd.md) -- [深漂 6 年了,回西安的一波面经总结✌️](docs/src/mianjing/chengxyspnhxagzl.md) +- [Java 基础八股文(背诵版)必看:+1:](docs/baguwen/java-basic.md) +- [Java 并发编程八股文(背诵版)必看:+1:](docs/baguwen/java-thread.md) +- [Java 虚拟机八股文(背诵版)必看:+1:](docs/baguwen/jvm.md) +- [MySQL 八股文(背诵版)必看:+1:](docs/sidebar/herongwei/mysql.md) +## 优质面经 + +- [春招斩获深圳腾讯PCG和杭州阿里云](docs/mianjing/shanganaliyun.md) +- [社招拿下阿里美团头条京东滴滴)](https://mp.weixin.qq.com/s/h2tV6v5Rh6jHdO9x0p63-g) +- [字节小姐姐的一份秋招攻略](https://mp.weixin.qq.com/s/0hCJy0m8nHm08HfyXKQT1A) +- [面试常见词汇扫盲+常见大厂面试特点分享](https://mp.weixin.qq.com/s/6TYEDM73N68vKXpmLRKhHA) +- [双非学历的社畜,历经 6 轮面试,最终拿下阿里Offer](https://mp.weixin.qq.com/s/vnMZY9Gsy3o1FwMi4f1GlA) + ## 面试准备 -- [面试常见词汇扫盲+大厂面试特点分享💪](docs/src/nice-article/weixin/miansmtgl.md) -- [有无实习/暑期实习 offer 如何准备秋招?💪](docs/src/nice-article/weixin/zijxjjdyfqzgl.md) -- [简历如何优化,简历如何投递,面试如何准备?💪](docs/src/nice-article/weixin/luoczbmsddyb.md) -- [校招时间节点、简历编写、笔试、HR面、实习等注意事项💪](docs/src/nice-article/weixin/youdxzhhmjzlycfx.md) +- [简历如何优化,简历如何投递,面试如何准备?](https://mp.weixin.qq.com/s/qurUqeD_VyiJRtB38vOuSw) +- [校招时间节点、简历编写、笔试、、HR面、实习等注意事项](https://mp.weixin.qq.com/s/rO7cU4NX74CoWADo_O4IUw) ## 城市选择 -- [武汉都有哪些值得加入的IT互联网公司?](docs/src/cityselect/wuhan.md) -- [北京都有哪些值得加入的IT互联网公司?](docs/src/cityselect/beijing.md) -- [广州都有哪些值得加入的IT互联网公司?](docs/src/cityselect/guangzhou.md) -- [深圳都有哪些值得加入的IT互联网公司?](docs/src/cityselect/shenzhen.md) -- [西安都有哪些值得加入的IT互联网公司?](docs/src/cityselect/xian.md) -- [青岛都有哪些值得加入的IT互联网公司?](docs/src/cityselect/qingdao.md) -- [郑州都有哪些值得加入的IT互联网公司?](docs/src/cityselect/zhengzhou.md) -- [苏州都有哪些值得加入的IT互联网公司?](docs/src/cityselect/suzhou.md) -- [南京都有哪些值得加入的IT互联网公司?](docs/src/cityselect/nanjing.md) -- [杭州都有哪些值得加入的IT互联网公司?](docs/src/cityselect/hangzhou.md) -- [成都都有哪些值得加入的IT互联网公司?](docs/src/cityselect/chengdu.md) -- [济南都有哪些值得加入的IT互联网公司?](docs/src/cityselect/jinan.md) +- [北京都有哪些牛逼的互联网公司?](docs/cityselect/beijing.md) +- [想去广州了!](docs/cityselect/guangzhou.md) +- [深圳有哪些牛批的互联网公司?](docs/cityselect/shenzhen.md) +- [西安有哪些不错的互联网公司?](docs/cityselect/xian.md) +- [青岛有牛逼的互联网公司吗?](docs/cityselect/qingdao.md) +- [郑州有哪些不错的互联网公司?](docs/cityselect/zhengzhou.md) +- [想搬去苏州生活了。](docs/cityselect/suzhou.md) +- [南京有哪些靠谱的互联网公司?](docs/cityselect/nanjing.md) +- [杭州有哪些顶级的互联网公司?](docs/cityselect/hangzhou.md) +- [成都有哪些牛批的互联网公司?](docs/cityselect/chengdu.md) + +## 工作体会 + # 学习资源 +> [!NOTE] > - **不知道学什么?不知道该怎么学?找不到优质的学习资源**?这些问题在这里统统都可以找到答案。 > - 我会把自己十多年的编程经验和学习资源毫不保留的分享出来。 ## PDF下载 -- [👏下载→30天速通 Java.pdf](docs/src/pdf/java30day.md) -- [👏下载→Linux速查备忘手册.pdf](docs/src/pdf/linux.md) -- [👏下载→超1000本计算机经典书籍分享](docs/src/pdf/java.md) -- [👏下载→2022年全网最全关于程序员学习和找工作的PDF资源](docs/src/pdf/programmer-111.md) -- [👏下载→深入浅出Java多线程PDF](docs/src/pdf/java-concurrent.md) -- [👏下载→GitHub星标115k+的Java教程](docs/src/pdf/github-java-jiaocheng-115-star.md) -- [👏下载→重学Java设计模式PDF](docs/src/pdf/shejimoshi.md) -- [👏下载→Java版LeetCode刷题笔记](docs/src/pdf/java-leetcode.md) -- [👏下载→阿里巴巴Java开发手册](docs/src/pdf/ali-java-shouce.md) -- [👏下载→阮一峰C语言入门教程](docs/src/pdf/yuanyifeng-c-language.md) -- [👏下载→BAT大佬的刷题笔记](docs/src/pdf/bat-shuati.md) -- [👏下载→给操作系统捋条线PDF](docs/src/pdf/os.md) -- [👏下载→豆瓣9.1分的Pro Git中文版](docs/src/pdf/progit.md) -- [👏下载→简历模板](docs/src/pdf/jianli.md) - -## 学习建议 +- [👏下载→Java程序员常读书单](docs/download/java.md) +- [👏下载→最全最硬核的Java面试 “备战” 资料](https://mp.weixin.qq.com/s/US5nTxbC2nYc1hWpn5Bozw) +- [👏下载→深入浅出Java多线程](https://mp.weixin.qq.com/s/pxKrjw_5NTdZfHOKCkwn8w) +- [👏下载→GitHub星标115k+的Java教程](https://mp.weixin.qq.com/s/d7Z0QoChNuP9bTwAGh2QCw) +- [👏下载→重学Java设计模式](https://mp.weixin.qq.com/s/PH5AizUAnTz0CuvJclpAKw) +- [👏下载→Java版LeetCode刷题笔记](https://mp.weixin.qq.com/s/FyoOPIMGcaeH0z5RMhxtaQ) +- [👏下载→阮一峰C语言入门教程](docs/download/yuanyifeng-c-language.md) +- [👏下载→BAT大佬的刷题笔记](docs/download/bat-shuati.md) +- [👏下载→给操作系统捋条线](https://mp.weixin.qq.com/s/puTGbgU7xQnRcvz5hxGBHA) +- [👏下载→豆瓣9.1分,Pro Git中文版](docs/download/progit.md) +- [👏下载→简历模板](docs/download/jianli.md) -- [计算机专业该如何自学编程,看哪些书籍哪些视频哪些教程?](docs/src/xuexijianyi/LearnCS-ByYourself.md) -- [如何阅读《深入理解计算机系统》这本书?](docs/src/xuexijianyi/read-csapp.md) -- [电子信息工程最好的出路的是什么?](docs/src/xuexijianyi/electron-information-engineering.md) -- [如何填报计算机大类高考填志愿,计科、人工智能、软工、大数据、物联网、网络工程该怎么选?](docs/src/xuexijianyi/gaokao-zhiyuan-cs.md) -- [测试开发工程师必读经典书籍有哪些?](docs/src/xuexijianyi/test-programmer-read-books.md) -- [校招 Java 后端开发应该掌握到什么程度?](docs/src/xuexijianyi/xiaozhao-java-should-master.md) -- [大裁员下,程序员如何做“副业”?](docs/src/xuexijianyi/chengxuyuan-fuye.md) -- [如何在繁重的工作中持续成长?](docs/src/xuexijianyi/ruhzfzdgzzcxcz.md) -- [如何获得高并发的经验?](docs/src/xuexijianyi/gaobingfa-jingyan-hsmcomputer.md) -- [怎么跟 HR 谈薪资?](docs/src/xuexijianyi/hr-xinzi.md) -- [程序员 35 岁危机,如何破局?](docs/src/xuexijianyi/35-weiji.md) -- [不到 20 人的 IT 公司该去吗?](docs/src/xuexijianyi/20ren-it-quma.md) -- [本科生如何才能进入腾讯、阿里等一流的互联网公司?](docs/src/xuexijianyi/benkesheng-ali-tengxun.md) -- [计算机考研 408 统考该如何准备?](docs/src/xuexijianyi/408.md) -# 知识库搭建 +# 知识库搭建历程 -> 从购买阿里云服务器+域名购买+域名备案+HTTP 升级到 HTTPS,全方面记录《二哥的Java进阶之路》知识库的诞生和改进过程,涉及到 docsify、Git、Linux 命令、GitHub 仓库等实用知识点。 +> [!NOTE] +> 从购买阿里云服务器+域名购买+域名备案+HTTP 升级到 HTTPS,全方面记录《Java 程序员进阶之路》知识库的诞生和改进过程,涉及到 docsify、Git、Linux 命令、GitHub 仓库等实用知识点。 -- [购买云服务器](docs/src/szjy/buy-cloud-server.md) -- [安装宝塔面板](docs/src/szjy/install-baota-mianban.md) -- [购买域名&域名解析](docs/src/szjy/buy-domain.md) -- [备案域名](docs/src/szjy/record-domain.md) -- [给域名配置HTTPS证书](docs/src/szjy/https-domain.md) -- [使用docsify+Git+GitHub+码云+阿里云服务器搭建知识库网站](docs/src/szjy/tobebetterjavaer-wangzhan-shangxian.md) +- [阿里云服务器购买+宝塔面板安装+域名购买+域名备案+升级HTTPS](docs/szjy/tobebetterjavaer-beian.md) +- [使用docsify+Git+GitHub+码云+阿里云服务器搭建知识库网站](docs/szjy/tobebetterjavaer-wangzhan-shangxian.md) -本知识库使用 VuePress 搭建,并基于[VuePress Theme Hope](https://theme-hope.vuejs.press/zh/)主题,你可以把[仓库](https://github.com/itwanger/toBeBetterJavaer)拉到本地后直接通过 `pnpm docs:clean-dev` 跑起来。 - ->前提是你已经安装好 node.js 和 pnpm 环境。 - -![pnpm 部署进阶之路](https://cdn.paicoding.com/stutymore/README-20241106103513.png) - -点击链接就可以在本地看到运行后的效果了。 - -![二哥的 Java 进阶之路首页](https://cdn.paicoding.com/stutymore/README-20230829162301.png) - -如果想部署服务器,可以执行 `pnpm docs:build` 打包生成 dist 目录,里面就是静态资源文件了。 - -执行 `zip -r dist.zip dist` 压缩为 dist.zip 包,然后上传到服务器的 Nginx 对应的静态资源目录下。再执行 `unzip dist.zip` 解压即可。 # 联系作者 +> [!NOTE] >- 作者是一名普通普通普通普通三连的 Java 后端开发者,热爱学习,热爱分享 >- 参加工作以后越来越理解交流和分享的重要性,在不停地汲取营养的同时,也希望帮助到更多的小伙伴们 ->- 二哥的Java进阶之路,不仅是作者自学 Java 以来所有的原创文章和学习资料的大聚合,更是作者向这个世界传播知识的一个窗口。 +>- Java 程序员进阶之路,不仅是作者自学 Java 以来所有的原创文章和学习资料的大聚合,更是作者向这个世界传播知识的一个窗口。 ## 心路历程 -- [走近作者:个人介绍 Q&A](docs/src/about-the-author/readme.md) -- [我的第一个,10 万(B站视频播放)](docs/src/about-the-author/bzhan-10wan.md) -- [我的第一个,一千万!知乎阅读](docs/src/about-the-author/zhihu-1000wan.md) -- [我的第二个,一千万!CSDN阅读](docs/src/about-the-author/csdn-1000wan.md) +- [走近作者:个人介绍 Q&A](docs/about-the-author/readme.md) +- [我的第一个,10 万(B站视频播放)](docs/about-the-author/bzhan-10wan.md) +- [我的第一个,一千万!知乎阅读](docs/about-the-author/zhihu-1000wan.md) +- [我的第二个,一千万!CSDN阅读](docs/about-the-author/csdn-1000wan.md) ## 联系方式 ### 原创公众号 -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - +本号的slogan:技术文通俗易懂,吹水文风趣幽默。
目前已有 10 万+读者关注,微信搜索「**沉默王二**」(也可以扫描下方的二维码)就可以关注作者了。 -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +
+ +
-![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +关注后,回复关键字「**00**」可以获取更多优质的 Java 学习资料。 ### star趋势图 @@ -609,202 +470,42 @@ GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https ### 友情链接 -- [paicoding](https://github.com/itwanger/paicoding),⭐️一款好用又强大的开源社区,附详细教程,包括Java、Spring、MySQL、Redis、微服务&分布式、消息队列、操作系统、计算机网络、数据结构与算法等计算机专业核心知识点。学编程,就上技术派😁。 - [Hippo4J](https://github.com/acmenlt/dynamic-threadpool),🔥 强大的动态线程池,附带监控报警功能(没有依赖中间件),完全遵循阿里巴巴编码规范。 - [JavaGuide](https://github.com/Snailclimb/JavaGuide),「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide! ### 捐赠鼓励 -开源不易,如果《二哥的Java进阶之路》对你有些帮助,可以请作者喝杯咖啡,算是对开源做出的一点点鼓励吧! +开源不易,如果《Java 程序员进阶之路》对你有些帮助,可以请作者喝杯咖啡,算是对开源做出的一点点鼓励吧!
- +
:gift_heart: 感谢大家对我资金的赞赏,每隔一个月会统计一次。 时间|小伙伴|赞赏金额 ---|---|--- -2025-07-02|橘子|4元 -2025-06-28|m*u|10元 -2025-06-15|l*y|5元 -2025-05-28|*航|6元 -2025-05-25|*星|10元 -2025-05-25|*(|6.66元 -2025-05-17|*鋈|4元 -2025-05-10|*庆|1元 -2025-05-08|芋*3|10元 -2025-04-17|*南|10元 -2025-03-31|:*D|4元 -2025-03-26|A*.|6.66元 -2025-02-18|R*.|6.66元 -2025-02-08|*金|5元 -2025-01-17|*蓝|8.88元 -2024-12-30|*甜|2元 -2024-12-26|*阳|1元 -2024-12-18|*。|1.5元 -2024-12-06|E*g|5元 -2024-12-04|*佚|0.88元 -2024-12-02|A*g|6.66元 -2024-11-30|1*0|10元 -2024-11-23|W*Z|11元 -2024-11-17|*旺|2元 -2024-11-16|*年|1元 -2024-11-14|*🤖|10元 -2024-11-13|*光|0.1元 -2024-10-25|*陈|1元 -2024-10-06|*天|10元 -2024-10-04|2*2|20元 -2024-09-25|c*l|1元 -2024-09-14|.*6|1.9元 -2024-08-16|*了|20元 -2024-08-14|*李|0.66元 -2024-08-12|*Z|6.66元 -2024-08-09|*峰|2元 -2024-07-13|*运|20元 -2024-07-01|*风|1元 -2024-06-30|*迷|1元 -2024-06-23|*瓦|1元 -2024-06-17|*芒|5元 -2024-06-13|*啊|9.99元 -2024-06-03|S*d|1元 -2024-05-23|*气|3元 -2024-05-22|w*r|6.6元 -2024-05-01|*笑|0.01元 -2024-04-24|1*0|3元 -2024-04-10|迷*x|21元 -2024-04-08|*青|5元 -2024-04-08|敲不出来的一个符号|1元 -2024-04-07|*i|0.01元 -2024-04-06|*牛|10元 -2024-04-03|Y*T|10元 -2024-04-02|B*E|2元 -2024-03-20|*卡|1元 -2024-03-18|*嘎|6.66元 -2024-03-17|*兴|0.01元 -2024-03-12|*鹏|0.02元 -2024-03-12|y*u|0.01元 -2024-02-29|r*y|6元 -2024-02-23|*~|9.99元 -2024-02-21|从头再来|5元 -2024-02-15|*斗|10元 -2024-02-02|*切|2元 -2024-02-01|*康|9元 -2024-01-31|*康|1元 -2024-01-22|*妙|10元 -2024-01-17|*清|9.9元 -2024-01-12|*奥|5元 -2024-01-04|*👈🏻|1元 -2024-01-03|*|3元 -2024-01-03|Y*o|2元 -2023-12-22|*逗|50元 -2023-11-25|*君|2元 -2023-10-23|*🐻|6.66元 -2023-10-17|*哈|5元 -2023-10-12|0*7|7.77元 -2023-10-03|S*d|0.5元 -2023-09-27|*1|1元 -2023-09-25|L*e|10.24元 -2023-09-19|*人|2元 -2023-09-15|L*D|2元 -2023-09-15|*暖|5元 -2023-09-11|A*B|1元 -2023-08-21|*氏|2元 -2023-08-18|*寻|1元 -2023-08-03|*案|10.24元 -2023-08-02|*,|1元 -2023-07-24|m*l|3元 -2023-07-20|lzy|6元 -2023-07-14|s*!|2元 -2023-07-02|*晴|1元 -2023-06-26|*雨|6.66元 -2023-06-21|*航|6元 -2023-06-21|*狼|3元 -2023-06-19|*定|2元 -2023-06-18|*道|5元 -2023-06-16|* 文|1元 -2023-06-14|G*e|66.6元 -2023-06-07|*.|0.5元 -2023-05-23|*W|5元 -2023-05-19|*飞|6元 -2023-05-10|c*r|1元 -2023-04-26|r*J|10.24元 -2023-04-22|*明|1元 -2023-04-09|* 刀|10元 -2023-04-03|*意|0.02元 -2023--03-17|*昌|8 元 -2023-03-16|~*~|66.6 元 -2023-03-15|*枫|6.6 元 -2023-03-10|十年|1 元 -2023-03-04|*风|5 元 -2023-02-26|一个表情(emoji)|1 元 -2023-02-23|曹*n|5元 -2023-02-11|昵称加载中.|6.6元 -2023-02-09|*明|10元 -2023-02-09|*风|5元 -2023-02-09|*z|3元 -2023-02-09|*夫|10元 -2023-02-08|*宝|5 元 -2023-01-18|*念|0.01元 -2023-01-18|*来|1元 -2023-01-10|*A*t|1元 -2023-01-07|*忠|5元 -2023-12-02|g*g|0.1元 -2022-11-13|*王|5元 -2022-11-10|*车|1元 -2022-11-10|F*k|1元 -2022-11-05|*H|3元 -2022-11-04|*金|0.02元 -2022-11-04|*尘|15元 -2022-11-02|*峰|1元 -2022-10-29|~*~|6元 -2022-10-28|k*k|1元 -2022-10-20|*电|2元 -2022-10-15|*深|5元 -2022-09-30|*君|1元 -2022-09-28|*懂|1元 -2022-09-27|*府|1元 -2022-09-23|*问号(emogji)|5元 -2022-09-23|H*n|1元 -2022-09-23|*a|0.01元 -2022-09-08|*👀|20元 -2022-09-07|丹*1|20元 -2022-08-27|*夹|40元 -2022-07-06|体*P|2元 -2022-07-05|*谦|5元 -2022-06-18|*杰|2元 -2022-06-15|L*c|15元 -2022-06-10|*❤|1元 -2022-06-09|'*'|1元 -2022-06-07|*勇|1元 -2022-06-03|*鸭|1元 2022-05-12|*烟|10元 2022-04-25|*思|5元 2022-04-20|w*n|1元 -2022-04-12|E*e|10 元 -2022-03-19|*风|9.9元 -2022-03-04|袁晓波|99元 +2022-04-12|*生|10元 +2022-03-04|袁*波|99元 2022-02-17|*色|1元 2022-02-17|M*y|1元 2022-01-28|G*R|6.6元 2022-01-20|*光|50元 2022-01-14|*浩|1元 -2022-01-01|刚*好|3.6元 2022-01-01|马*谊|6.6元 +2022-01-01|刚*好|3.6元 2021-12-20|t*1|5 元 -2021-10-26|*猫|28 元 +2021-10-26|*鱼|28 元 2021-10-11|*人|28 元 -2021-09-28|*人|1 元 -2021-09-05|N*a|3 元 -2021-09-02|S*n|6.6 元 -2021-08-21|z*s|3 元 -2021-08-20|A*g|10 元 -2021-08-09|*滚|0.1 元 +2021-09-01|S*n|6.6 元 2021-08-02|*秒|1 元 2021-06-13|*7| 28 元 -2021-05-04|*学|169 元 2021-04-29|p*e|2 元 -2021-04-28|追风筝的神|1 元 + + ### 参与贡献 diff --git a/_coverpage.md b/_coverpage.md new file mode 100644 index 0000000000..ff76d96987 --- /dev/null +++ b/_coverpage.md @@ -0,0 +1,31 @@ +![logo](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/logo.png) + + +# Java程序员进阶之路 To Be Better Javaer + + +> 一份通俗易懂、风趣幽默的Java学习指南,内容涵盖Java基础、Java并发编程、Java虚拟机、Java企业级开发、Java面试等核心知识点。学Java,就认准Java 程序员进阶之路😄 + + +

+ + + + + 无套路下载 + +

+ + + + 👁️本页总访问次数: + + + | 🧑总访客数: + + +[联系作者](https://mp.weixin.qq.com/s/1_lOGt4Fl6Yy8iVdxWeI5g) +开始阅读 + + + diff --git a/_navbar.md b/_navbar.md new file mode 100644 index 0000000000..779f04e6c5 --- /dev/null +++ b/_navbar.md @@ -0,0 +1,16 @@ +* [计算机经典书籍下载](https://tobebetterjavaer.com/download/java.html) +* [B站视频](https://space.bilibili.com/513340480) +* [尝试新版](https://tobebetterjavaer.com/) +* 学习路线 + * [Java学习路线一条龙版](https://tobebetterjavaer.com/xuexiluxian/java/yitiaolong.html) + * [Java并发学习路线](https://tobebetterjavaer.com/xuexiluxian/java-thread.html) + * [Java虚拟机学习路线](https://tobebetterjavaer.com/xuexiluxian/java/jvm.html) + * [C语言学习路线](https://tobebetterjavaer.com/xuexiluxian/c.html) + * [C++学习路线](https://tobebetterjavaer.com/xuexiluxian/ccc.html) + * [Python学习路线](https://tobebetterjavaer.com/xuexiluxian/python.html) + * [Go语言学习路线](https://tobebetterjavaer.com/xuexiluxian/go.html) + * [操作系统学习路线](https://tobebetterjavaer.com/xuexiluxian/os.html) + * [前端学习路线](https://tobebetterjavaer.com/xuexiluxian/qianduan.html) + * [蓝桥杯学习路线](https://tobebetterjavaer.com/xuexiluxian/lanqiaobei.html) + * [算法和数据结构学习路线](https://tobebetterjavaer.com/xuexiluxian/algorithm.html) + diff --git a/_sidebar.md b/_sidebar.md new file mode 100644 index 0000000000..360c465abf --- /dev/null +++ b/_sidebar.md @@ -0,0 +1,49 @@ +* [为什么会有这个开源知识库](README.md?id=为什么会有这个开源知识库) +* [知识库地图](README.md?id=知识库地图) +* [学习路线](README.md?id=学习路线) +* [Java核心](README.md?id=java核心) + * [Java面渣逆袭](README.md?id=Java面渣逆袭) + * [Java概述](README.md?id=java概述) + * [Java基础语法](README.md?id=java基础语法) + * [Java面向对象编程](README.md?id=Java面向对象编程) + * [字符串&数组](README.md?id=字符串&数组) + * [集合框架(容器)](README.md?id=集合框架(容器)) + * [Java输入输出](README.md?id=Java输入输出) + * [异常处理](README.md?id=异常处理) + * [常用工具类](README.md?id=常用工具类) + * [Java新特性](README.md?id=Java新特性) + * [Java重要知识点](README.md?id=java重要知识点) + * [Java并发编程](README.md?id=Java并发编程) + * [Java虚拟机](README.md?id=Java虚拟机) +* [Java企业级开发](README.md?id=Java企业级开发) + * [开发工具](README.md?id=开发工具) + * [IDE/编辑器](README.md?id=IDE/编辑器) + * [Spring](README.md?id=Spring) + * [SpringBoot](README.md?id=SpringBoot) + * [辅助工具/轮子](README.md?id=辅助工具) + * [安全篇](README.md?id=安全篇) + * [分布式](README.md?id=分布式) + * [高性能](README.md?id=高性能) + * [高可用](README.md?id=高可用) +* [数据库](README.md?id=数据库) + * [MySQL](README.md?id=MySQL) + * [Redis](README.md?id=Redis) + * [MongoDB](README.md?id=MongoDB) +* [计算机基础](README.md?id=计算机基础) +* [求职面试](README.md?id=求职面试) + * [面试题集合](README.md?id=面试题集合) + * [背诵版八股文](README.md?id=背诵版八股文) + * [优质面经](README.md?id=优质面经) + * [面试准备](README.md?id=面试准备) + * [城市选择](README.md?id=城市选择) +* [学习资源](README.md?id=学习资源) + * [PDF下载](README.md?id=PDF下载) + * [图文教程](README.md?id=图文教程) + * [视频教程](README.md?id=视频教程) + * [优质书单](README.md?id=优质书单) + * [学习建议](README.md?id=学习建议) +* [知识库建设](README.md?id=知识库建设) +* [联系作者](README.md?id=联系作者) + * [心路历程](README.md?id=心路历程) + * [联系方式](README.md?id=联系方式) + diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts new file mode 100644 index 0000000000..c65e200a06 --- /dev/null +++ b/docs/.vuepress/config.ts @@ -0,0 +1,40 @@ +import { defineHopeConfig } from "vuepress-theme-hope"; +import themeConfig from "./themeConfig"; + +export default defineHopeConfig({ + base: "/", + + dest: "./dist", + + head: [ + [ + "script",{}, + ` + var _hmt = _hmt || []; + (function() { + var hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?5230ac143650bf5eb3c14f3fb9b1d3ec"; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + })(); + ` + ], + [ + "link", + { + rel: "stylesheet", + href: "//at.alicdn.com/t/font_3180624_7cy10l7jqqh.css", + }, + ], + ], + + locales: { + "/": { + lang: "zh-CN", + title: "Java 程序员进阶之路", + description: "一份通俗易懂、风趣幽默的Java学习指南,内容涵盖Java基础、Java并发编程、Java虚拟机、Java企业级开发、Java面试等核心知识点。学Java,就认准Java程序员进阶之路", + }, + }, + + themeConfig, +}); diff --git a/docs/.vuepress/navbar.ts b/docs/.vuepress/navbar.ts new file mode 100644 index 0000000000..7a02dd1fae --- /dev/null +++ b/docs/.vuepress/navbar.ts @@ -0,0 +1,67 @@ +import { defineNavbarConfig } from "vuepress-theme-hope"; + +export default defineNavbarConfig([ + + { + text: "进阶之路", + icon: "lujing", + link: "/home.md" + }, + { + text: "星球专栏", + icon: "Artboard", + link: "/zhishixingqiu/java-mianshi-zhinan.md" + }, + { + text: "学习路线", + icon: "luxian", + link: "/xuexiluxian/" + }, + { + text: "B站视频", + icon: "bzhan", + link: "https://space.bilibili.com/513340480" + }, + { + text: "珍藏资源", + icon: "youzhi", + children: [ + { + text: "Java电子书下载", + icon: "java", + link: "/download/java.md" + }, + { + text: "PDF干货笔记下载", + icon: "pdf", + link: "/download/pdf.md" + }, + { + text: "学习建议", + icon: "xuexijianyi", + link: "/download/learn-jianyi.md" + }, + { + text: "面渣逆袭", + icon: "zhunbei", + link: "/sidebar/sanfene/nixi.md" + }, + { + text: "优质文章", + icon: "youzhi", + link: "/download/nicearticle.md" + }, + { + text: "网络日志", + icon: "rizhi", + link: "/download/history.md" + }, + { + text: "回到过去", + icon: "fanhuijiuban", + link: "https://docsify.tobebetterjavaer.com/" + }, + ], + }, + +]); diff --git a/docs/src/.vuepress/public/assets/icon/apple-icon-152.png b/docs/.vuepress/public/assets/icon/apple-icon-152.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/apple-icon-152.png rename to docs/.vuepress/public/assets/icon/apple-icon-152.png diff --git a/docs/src/.vuepress/public/assets/icon/chrome-192.png b/docs/.vuepress/public/assets/icon/chrome-192.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/chrome-192.png rename to docs/.vuepress/public/assets/icon/chrome-192.png diff --git a/docs/src/.vuepress/public/assets/icon/chrome-512.png b/docs/.vuepress/public/assets/icon/chrome-512.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/chrome-512.png rename to docs/.vuepress/public/assets/icon/chrome-512.png diff --git a/docs/src/.vuepress/public/assets/icon/chrome-mask-192.png b/docs/.vuepress/public/assets/icon/chrome-mask-192.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/chrome-mask-192.png rename to docs/.vuepress/public/assets/icon/chrome-mask-192.png diff --git a/docs/src/.vuepress/public/assets/icon/chrome-mask-512.png b/docs/.vuepress/public/assets/icon/chrome-mask-512.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/chrome-mask-512.png rename to docs/.vuepress/public/assets/icon/chrome-mask-512.png diff --git a/docs/src/.vuepress/public/assets/icon/itwanger-282.png b/docs/.vuepress/public/assets/icon/itwanger-282.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/itwanger-282.png rename to docs/.vuepress/public/assets/icon/itwanger-282.png diff --git a/docs/src/.vuepress/public/assets/icon/itwanger-maskable.png b/docs/.vuepress/public/assets/icon/itwanger-maskable.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/itwanger-maskable.png rename to docs/.vuepress/public/assets/icon/itwanger-maskable.png diff --git a/docs/src/.vuepress/public/assets/icon/itwanger-monochrome.png b/docs/.vuepress/public/assets/icon/itwanger-monochrome.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/itwanger-monochrome.png rename to docs/.vuepress/public/assets/icon/itwanger-monochrome.png diff --git a/docs/src/.vuepress/public/assets/icon/ms-icon-144.png b/docs/.vuepress/public/assets/icon/ms-icon-144.png similarity index 100% rename from docs/src/.vuepress/public/assets/icon/ms-icon-144.png rename to docs/.vuepress/public/assets/icon/ms-icon-144.png diff --git a/docs/src/.vuepress/public/favicon.ico b/docs/.vuepress/public/favicon.ico similarity index 100% rename from docs/src/.vuepress/public/favicon.ico rename to docs/.vuepress/public/favicon.ico diff --git a/docs/src/.vuepress/public/logo.png b/docs/.vuepress/public/logo.png similarity index 100% rename from docs/src/.vuepress/public/logo.png rename to docs/.vuepress/public/logo.png diff --git a/docs/src/.vuepress/public/logo.svg b/docs/.vuepress/public/logo.svg similarity index 100% rename from docs/src/.vuepress/public/logo.svg rename to docs/.vuepress/public/logo.svg diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts new file mode 100644 index 0000000000..ae21d0880d --- /dev/null +++ b/docs/.vuepress/sidebar.ts @@ -0,0 +1,1019 @@ +import { defineSidebarConfig } from "vuepress-theme-hope"; +export const sidebarConfig = defineSidebarConfig({ + "/zhishixingqiu/": ["java-mianshi-zhinan","readme.md"], + "/download/": ["java","pdf","learn-jianyi","nicearticle", "history"], + "/xuexiluxian/": [ + { + text: "Java", + icon: "java", + prefix: "java/", + collapsable: true, + children: [ + { + text: "一条龙版", + link: "yitiaolong.md", + }, + { + text: "并发编程", + link: "thread.md", + }, + { + text: "JVM", + link: "jvm.md", + }, + ], + }, + { + text: "C语言", + link: "c.md", + }, + { + text: "C++", + link: "ccc.md", + }, + { + text: "Python", + link: "python.md", + }, + { + text: "Go语言", + link: "go.md", + }, + { + text: "操作系统", + link: "os.md", + }, + { + text: "前端", + link: "qianduan.md", + }, + { + text: "蓝桥杯", + link: "lanqiaobei.md", + }, + { + text: "算法和数据结构", + link: "algorithm.md", + }, + ], + "/sidebar/sanfene/": [ + { + text: "Java 基础", + link: "javase.md", + }, + { + text: "Java 集合框架", + link: "collection.md", + }, + { + text: "Java 并发编程", + link: "javathread.md", + }, + { + text: "Java 虚拟机", + link: "jvm.md", + }, + { + text: "Spring", + link: "spring.md", + }, + { + text: "Redis", + link: "redis.md", + }, + ], + // 必须放在最后面 + "/": [ + { + text: "一、前言", + link: "home.md", + }, + { + text: "二、Java核心", + collapsable: true, + children: [ + { + prefix: "overview/", + text: "2.1 Java概述", + collapsable: true, + children: [ + "what-is-java", + { + text: "编写第一个 Java 程序", + link: "hello-world", + }, + ], + }, + { + text: "2.2 Java基础语法", + collapsable: true, + children: [ + { + text: "基本数据类型", + link: "basic-grammar/basic-data-type", + }, + { + text: "流程控制语句", + link: "basic-grammar/flow-control", + }, + { + text: "运算符", + link: "basic-grammar/operator", + }, + { + text: "注释", + link: "basic-grammar/javadoc", + }, + { + text: "关键字", + link: "basic-extra-meal/48-keywords", + }, + { + text: "命名规范", + link: "basic-extra-meal/java-naming", + }, + ], + }, + { + text: "2.3 面向对象编程", + collapsable: true, + children: [ + { + text: "对象和类", + link: "oo/object-class", + }, + { + text: "变量", + link: "oo/var", + }, + { + text: "方法", + link: "oo/method", + }, + { + text: "构造方法", + link: "oo/construct", + }, + { + text: "代码初始化块", + link: "oo/code-init", + }, + { + text: "抽象类", + link: "oo/abstract", + }, + { + text: "接口", + link: "oo/interface", + }, + { + text: "内部类", + link: "oo/inner-class", + }, + { + text: "static", + link: "oo/static", + }, + { + text: "this 和 super", + link: "oo/this-super", + }, + { + text: "final", + link: "oo/final", + }, + { + text: "instanceof", + link: "oo/instanceof", + }, + { + text: "不可变对象", + link: "basic-extra-meal/immutable", + }, + { + text: "可变参数", + link: "basic-extra-meal/varables", + }, + { + text: "泛型", + link: "basic-extra-meal/generic", + }, + { + text: "注解", + link: "basic-extra-meal/annotation", + }, + { + text: "枚举", + link: "basic-extra-meal/enum", + }, + { + text: "反射", + link: "basic-extra-meal/fanshe", + }, + + ], + }, + { + text: "2.4 字符串&数组", + collapsable: true, + children: [ + + { + text: "字符串为什么是不可变的", + link: "string/immutable", + }, + { + text: "字符串常量池", + link: "string/constant-pool", + }, + { + text: " String#intern", + link: "string/intern", + }, + { + text: "字符串比较", + link: "string/equals", + }, + { + text: "字符串拼接", + link: "string/join", + }, + { + text: "字符串分割", + link: "string/split", + }, + { + text: "数组", + link: "array/array", + }, + { + text: "打印数组", + link: "array/print", + }, + + ], + }, + { + text: "2.5 集合框架(容器)", + collapsable: true, + children: [ + + { + text: "概述", + link: "collection/gailan", + }, + { + text: "ArrayList", + link: "collection/arraylist", + }, + { + text: "LinkedList", + link: "collection/linkedlist", + }, + { + text: "ArrayList和LinkedList的区别", + link: "collection/list-war-2", + }, + { + text: "Iterator和Iterable", + link: "collection/iterator-iterable", + }, + { + text: "fail-fast", + link: "collection/fail-fast", + }, + { + text: "HashMap", + link: "collection/hashmap", + }, + + ], + }, + { + text: "2.6 IO", + collapsable: true, + prefix:"io/", + children: [ + { + text: "概览", + link: "shangtou", + }, + { + text: "BIO、NIO和AIO", + link: "BIONIOAIO", + }, + ], + }, + { + text: "2.7 异常处理", + collapsable: true, + prefix:"exception/", + children: [ + { + text: "概览", + link: "gailan", + }, + { + text: "try-with-resouces", + link: "try-with-resouces", + }, + { + text: "最佳实践", + link: "shijian", + }, + { + text: "NullPointerException", + link: "npe", + }, + ], + }, + { + text: "2.8 常用工具类", + collapsable: true, + prefix:"common-tool/", + children: [ + { + text: "Arrays", + link: "arrays", + }, + { + text: "Collections", + link: "collections", + }, + { + text: "Hutool", + link: "hutool", + }, + { + text: "Guava", + link: "guava", + }, + ], + }, + { + text: "2.9 Java新特性", + prefix: "java8/", + collapsable: true, + children: [ + { + text: "Stream", + link: "stream", + }, + { + text: "Optional", + link: "optional", + }, + { + text: "Lambda", + link: "Lambda", + }, + ], + }, + { + text: "2.10 Java重要知识点", + prefix:"basic-extra-meal/", + collapsable: true, + children: [ + { + text: "Unicode和UTF-8编码", + link: "java-unicode", + }, + { + text: "new Integer和Integer.valueOf区别", + link: "int-cache", + }, + { + text: "拆箱和装箱", + link: "box", + }, + { + text: "浅拷贝与深拷贝", + link: "deep-copy", + }, + { + text: "深入理解Java中的hashCode方法", + link: "hashcode", + }, + { + text: "重写equals时为什么要重写hashCode", + link: "equals-hashcode", + }, + { + text: "重写和重载的区别", + link: "override-overload", + }, + { + text: "重写时应当遵守的11条规则", + link: "Overriding", + }, + { + text: "Java到底是值传递还是引用传递", + link: "pass-by-value", + }, + { + text: "Java为什么不能实现真正的泛型", + link: "true-generic", + }, + { + text: "Comparable和Comparator的区别", + link: "comparable-omparator", + }, + { + text: "JDK9中String为什么由char[]改成byte[]", + link: "jdk9-char-byte-string", + }, + { + text: "JDK源码无限循环用for(;;)还是while(true)", + link: "jdk-while-for-wuxian-xunhuan", + }, + { + text: "先有Class还是先有Object", + link: "class-object", + }, + { + text: "instanceof关键字是如何实现的", + link: "instanceof-jvm", + }, + ], + }, + { + text: "2.11 并发编程", + collapsable: true, + prefix: "thread/", + children: [ + { + text: "创建Java线程的3种方式", + link: "wangzhe-thread", + }, + { + text: "线程的6种状态及切换", + link: "thread-state-and-method", + }, + { + text: "线程组和线程优先级", + link: "thread-group-and-thread-priority", + }, + { + text: "进程与线程的区别", + link: "why-need-thread", + }, + { + text: "并发编程带来了哪些问题", + link: "thread-bring-some-problem", + }, + { + text: "Java内存模型", + link: "jmm", + }, + { + text: "volatile", + link: "volatile", + }, + { + text: "synchronized", + link: "synchronized", + }, + { + text: "CAS的原理", + link: "cas", + }, + { + text: "AQS详解", + link: "aqs", + }, + { + text: "锁", + link: "lock", + }, + { + text: "重入锁ReentrantLock", + link: "reentrantLock", + }, + { + text: "读写锁ReentrantReadWriteLock", + link: "ReentrantReadWriteLock", + }, + { + text: "线程协作类Condition", + link: "condition", + }, + { + text: "线程阻塞唤醒类LockSupport", + link: "LockSupport", + }, + { + text: "并发集合容器", + link: "map", + }, + { + text: "ConcurrentHashMap", + link: "ConcurrentHashMap", + }, + { + text: "ConcurrentLinkedQueue", + link: "ConcurrentLinkedQueue", + }, + { + text: "CopyOnWriteArrayList", + link: "CopyOnWriteArrayList", + }, + { + text: "ThreadLocal", + link: "ThreadLocal", + }, + { + text: "BlockingQueue", + link: "BlockingQueue", + }, + { + text: "线程池", + link: "pool", + }, + { + text: "计划任务", + link: "ScheduledThreadPoolExecutor", + }, + { + text: "原子操作类", + link: "atomic", + }, + { + text: "通信工具类CountDownLatch", + link: "CountDownLatch", + }, + { + text: "Fork/Join框架", + link: "fork-join", + }, + { + text: "生产者-消费者模式", + link: "shengchanzhe-xiaofeizhe", + }, + + ], + }, + { + text: "2.12 JVM", + prefix: "jvm/", + collapsable: true, + children: [ + { + text: "JVM到底是什么?", + link: "what-is-jvm", + }, + { + text: "JVM到底是如何运行Java代码的", + link: "how-run-java-code", + }, + { + text: "类加载机制", + link: "class-load", + }, + { + text: "详解Java的类文件结构", + link: "class-file-jiegou", + }, + { + text: "从javap的角度轻松看懂字节码", + link: "bytecode", + }, + { + text: "字节码指令详解", + link: "zijiema-zhiling", + }, + { + text: "虚拟机是如何执行字节码指令的", + link: "how-jvm-run-zijiema-zhiling", + }, + { + text: "HSDB(Hotspot Debugger)", + link: "hsdb", + }, + { + text: "史上最通俗易懂的ASM教程", + link: "asm", + }, + { + text: "自己编译JDK", + link: "compile-jdk", + }, + { + text: "深入理解JVM的内存结构", + link: "neicun-jiegou", + }, + { + text: "Java 创建的对象到底放在哪", + link: "whereis-the-object", + }, + { + text: "从头到尾说一次Java垃圾回收", + link: "gc", + }, + { + text: "图解Java的垃圾回收机制", + link: "tujie-gc", + }, + { + text: "Java问题诊断和排查工具", + link: "problem-tools", + }, + { + text: "JIT原理解析及实践", + link: "jit", + }, + { + text: "内存溢出排查优化实战", + link: "oom", + }, + { + text: "CPU 100% 排查优化实践", + link: "cpu-percent-100", + }, + { + text: "JVM 核心知识点总结", + link: "zongjie", + }, + + ], + }, + ], + }, + { + text: "三、Java企业级开发", + collapsable: true, + children: [ + { + text: "3.1 开发工具", + collapsable: true, + children: [ + "maven/maven.md", + "git/git-qiyuan.md", + "nginx/nginx.md", + ], + }, + { + text: "3.2 IDE/编辑器", + collapsable: true, + children: [ + "ide/4-debug-skill.md", + ], + }, + { + text: "3.3 Spring", + collapsable: true, + children: [ + { + text: "Spring AOP扫盲", + link: "springboot/aop-log", + }, + { + text: "Spring IoC扫盲", + link: "springboot/ioc", + }, + ], + }, + { + text: "3.4 SpringBoot", + collapsable: true, + children: [ + { + text: "搭建第一个Spring Boot项目", + link: "springboot/initializr", + }, + { + text: "整合MySQL和Druid", + link: "springboot/mysql-druid", + }, + { + text: "整合JPA", + link: "springboot/jpa", + }, + { + text: "整合Thymeleaf", + link: "springboot/thymeleaf", + }, + { + text: "开启事务支持", + link: "springboot/transaction", + }, + { + text: "过滤器、拦截器、监听器", + link: "springboot/Filter-Interceptor-Listener", + }, + { + text: "整合Redis实现缓存", + link: "redis/redis-springboot", + }, + { + text: "整合Logback", + link: "springboot/logback" + }, + { + text: "整合Swagger-UI", + link: "springboot/swagger" + }, + { + text: "整合Knife4j", + link: "gongju/knife4j" + }, + { + text: "整合SpringTask", + link: "springboot/springtask" + }, + { + text: "整合MyBatis-Plus AutoGenerator", + link: "kaiyuan/auto-generator", + }, + + ], + }, + { + text: "3.5 辅助工具/轮子", + collapsable: true, + children: [ + { + text: "Tabby", + link: "gongju/tabby", + }, + { + text: "Warp", + link: "gongju/warp", + }, + { + text: "Chocolatey", + link: "gongju/choco", + }, + { + text: "chiner", + link: "gongju/chiner", + }, + { + text: "DBeaver", + link: "gongju/DBeaver", + }, + { + text: "Forest", + link: "gongju/forest", + }, + { + text: "Junit", + link: "gongju/junit", + }, + { + text: "fastjson", + link: "gongju/fastjson", + }, + { + text: "Gson", + link: "gongju/gson", + }, + { + text: "Jackson", + link: "gongju/jackson", + }, + { + text: "Log4j", + link: "gongju/log4j", + }, + { + text: "Log4j2", + link: "gongju/log4j2", + }, + { + text: "Logback", + link: "gongju/logback", + }, + { + text: "SLF4J", + link: "gongju/slf4j", + }, + + ], + }, + { + text: "3.6 分布式", + collapsable: true, + children: [ + { + text: "Elasticsearch入门", + link: "elasticsearch/rumen" + }, + { + text: "聊聊ZooKeeper", + link: "zookeeper/jibenjieshao" + }, + { + text: "聊聊微服务网关", + link: "microservice/api-wangguan" + }, + ], + }, + { + text: "3.7 消息队列", + collapsable: true, + children: [ + { + text: "RabbitMQ入门", + link: "mq/rabbitmq-rumen" + }, + { + text: "如何保障消息不丢失", + link: "mq/100-budiushi" + }, + ], + }, + ], + }, + { + text: "四、数据库", + collapsable: true, + children: [ + { + text: "MySQL", + collapsable: true, + children: [ + { + text: "MySQL和Redis数据一致性", + link: "mysql/redis-shuju-yizhixing" + }, + ], + }, + { + text: "Redis", + collapsable: true, + children: [ + { + text: "Redis入门", + link: "redis/rumen" + }, + { + text: "缓存雪崩、穿透、击穿", + link: "redis/xuebeng-chuantou-jichuan" + }, + ], + }, + { + text: "MongoDB", + collapsable: true, + children: [ + "mongodb/rumen", + ], + }, + ], + }, + { + text: "五、计算机基础", + collapsable: true, + prefix: "cs/", + children: [ + { + text: "计算机操作系统", + link: "os", + }, + { + text: "计算机网络", + link: "wangluo", + }, + ], + }, + { + text: "六、求职面试", + collapsable: true, + children: [ + { + text: "面试题集合", + collapsable: true, + children: [ + "baguwen/java-basic-34", + "collection/hashmap-interview", + "mianjing/redis12question", + "nginx/40-interview" + ], + }, + { + text: "背诵版八股文", + collapsable: true, + children: [ + "baguwen/java-basic", + "baguwen/java-thread", + "baguwen/jvm", + "sidebar/herongwei/mysql", + ], + }, + { + text: "城市选择", + prefix: "cityselect/", + collapsable: true, + children: [ + "beijing", + "chengdu", + "guangzhou", + "hangzhou", + "nanjing", + "qingdao", + "shenzhen", + "suzhou", + "xian", + "zhengzhou", + ], + }, + ], + }, + { + text: "七、学习资源", + collapsable: true, + children: [ + { + text: "PDF下载", + collapsable: true, + children: [ + { + text: "Java程序员常读书单", + icon: "xiazai", + link: "download/java.md", + }, + { + text: "最全最硬核的Java面试 “备战” 资料", + icon: "xiazai", + link: "https://mp.weixin.qq.com/s/US5nTxbC2nYc1hWpn5Bozw", + }, + { + text: "深入浅出Java多线程", + icon: "xiazai", + link: "https://mp.weixin.qq.com/s/pxKrjw_5NTdZfHOKCkwn8w", + }, + { + text: "GitHub星标115k+的Java教程", + icon: "xiazai", + link: "https://mp.weixin.qq.com/s/d7Z0QoChNuP9bTwAGh2QCw", + }, + { + text: "重学Java设计模式", + icon: "xiazai", + link: "https://mp.weixin.qq.com/s/PH5AizUAnTz0CuvJclpAKw", + }, + { + text: "Java版LeetCode刷题笔记", + icon: "xiazai", + link: "https://mp.weixin.qq.com/s/FyoOPIMGcaeH0z5RMhxtaQ", + }, + { + text: "阮一峰C语言入门教程", + icon: "xiazai", + link: "download/yuanyifeng-c-language.md", + }, + { + text: "BAT大佬的刷题笔记", + icon: "xiazai", + link: "download/bat-shuati.md", + }, + { + text: "给操作系统捋条线", + icon: "xiazai", + link: "https://mp.weixin.qq.com/s/puTGbgU7xQnRcvz5hxGBHA", + }, + { + text: "豆瓣9.1分,Pro Git中文版", + icon: "xiazai", + link: "download/progit.md", + }, + { + text: "简历模板", + icon: "xiazai", + link: "download/jianli.md", + }, + ], + }, + { + text: "学习建议", + collapsable: true, + prefix: "xuexijianyi/", + children: [ + "read-csapp", + "electron-information-engineering", + ], + }, + ], + }, + { + text: "八、联系作者", + collapsable: true, + children: [ + { + text: "心路历程", + prefix: "about-the-author/", + collapsable: true, + children: [ + "bzhan-10wan", + "zhihu-1000wan", + "csdn-1000wan", + "readme.md", + ], + }, + ], + }, + ], +}); + + + diff --git a/docs/.vuepress/styles/config.scss b/docs/.vuepress/styles/config.scss new file mode 100644 index 0000000000..3726b05ce8 --- /dev/null +++ b/docs/.vuepress/styles/config.scss @@ -0,0 +1,2 @@ +$codeLightTheme: "one-light"; +$codeDarkTheme: "one-dark" \ No newline at end of file diff --git a/docs/.vuepress/styles/index.scss b/docs/.vuepress/styles/index.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/.vuepress/styles/palette.scss b/docs/.vuepress/styles/palette.scss new file mode 100644 index 0000000000..efbb0e7f61 --- /dev/null +++ b/docs/.vuepress/styles/palette.scss @@ -0,0 +1,4 @@ +// colors +$themeColor: #5b86ff; +$sidebarMobileWidth: 16rem; +$sidebarWidth: 20rem; \ No newline at end of file diff --git a/docs/.vuepress/themeConfig.ts b/docs/.vuepress/themeConfig.ts new file mode 100644 index 0000000000..08a2f13caf --- /dev/null +++ b/docs/.vuepress/themeConfig.ts @@ -0,0 +1,177 @@ +import { defineThemeConfig } from "vuepress-theme-hope"; +import navbar from "./navbar"; +import { sidebarConfig } from "./sidebar"; + +export default defineThemeConfig({ + hostname: "https://tobebetterjavaer.com", + + author: { + name: "沉默王二", + url: "https://tobebetterjavaer.com", + }, + + iconPrefix: "iconfont icon-", + + logo: "http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/logo-02.png", + + repo: "https://github.com/itwanger/toBeBetterJavaer", + + docsDir: "docs", + + // 以前的默认仓库分支 + docsBranch: "master", + + // 纯净模式 + // pure: true, + + darkmode: "switch", + + // navbar + navbar: navbar, + + // sidebar + sidebar: sidebarConfig, + + footer: '豫ICP备2021038026号-1' + +'' + +'' + +'豫公网安备 41030502000411号' + +'', + + displayFooter: true, + + pageInfo: ["Author", "Original", "Date", "Category", "Tag", "ReadingTime"], + + blog: { + intro: "/about-the-author/", + sidebarDisplay: "mobile", + autoExcerpt: true, + avatar: "/assets/icon/itwanger-282.png", + description:"没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。", + medias: { + Zhihu: "https://www.zhihu.com/people/cmower", + Github: "https://github.com/itwanger", + Gitee: "https://gitee.com/itwanger", + }, + }, + + plugins: { + // 评论区 + comment: { + type: "giscus", + repo :"itwanger/tobebetterjavaer-giscus", + repoId:"R_kgDOHBJssg", + category:"Announcements", + categoryId:"DIC_kwDOHBJsss4COJOx", + mapping:"pathname", + inputPositio:"bottom" + }, + docsearch: { + appId: "O566AMFNJH", + apiKey: "d9aebea8bd1a4f1e01201464bbab255f", + indexName: "tobebetterjavaer", + locales: { + "/": { + placeholder: "搜索文档", + translations: { + button: { + buttonText: "搜索文档", + buttonAriaLabel: "搜索文档", + }, + modal: { + searchBox: { + resetButtonTitle: "清除查询条件", + resetButtonAriaLabel: "清除查询条件", + cancelButtonText: "取消", + cancelButtonAriaLabel: "取消", + }, + startScreen: { + recentSearchesTitle: "搜索历史", + noRecentSearchesText: "没有搜索历史", + saveRecentSearchButtonTitle: "保存至搜索历史", + removeRecentSearchButtonTitle: "从搜索历史中移除", + favoriteSearchesTitle: "收藏", + removeFavoriteSearchButtonTitle: "从收藏中移除", + }, + errorScreen: { + titleText: "无法获取结果", + helpText: "你可能需要检查你的网络连接", + }, + footer: { + selectText: "选择", + navigateText: "切换", + closeText: "关闭", + searchByText: "搜索提供者", + }, + noResultsScreen: { + noResultsText: "无法找到相关结果", + suggestedQueryText: "你可以尝试查询", + openIssueText: "你认为该查询应该有结果?", + openIssueLinkText: "点击反馈", + }, + }, + }, + }, + }, + }, + + + blog: { + // 生成摘要 + autoExcerpt: true, + }, + + activeHeaderLinks: true, + + mdEnhance: { + // 仅将此选项用于体验或测试。 + align: true, + presentation: { + plugins: ["highlight", "math", "search", "notes", "zoom"], + }, + }, + + // Progressive Web app,即渐进式网络应用程序, + // 允许网站通过支持该特性的浏览器将网站作为 App 安装在对应平台上。 + pwa: { + // favicon.ico一般用于作为缩略的网站标志,它显示位于浏览器的地址栏或者在标签上,用于显示网站的logo, + favicon: "/favicon.ico", + // 如果你的站点体积不大,且配图大多为关键性说明,希望可以在离线模式下显示,建议将此项设置为 true + cachePic: true, + apple: { + icon: "/assets/icon/apple-icon-152.png", + statusBarColor: "black", + }, + msTile: { + image: "/assets/icon/ms-icon-144.png", + color: "#ffffff", + }, + manifest: { + icons: [ + { + src: "/assets/icon/chrome-mask-512.png", + sizes: "512x512", + purpose: "maskable", + type: "image/png", + }, + { + src: "/assets/icon/chrome-mask-192.png", + sizes: "192x192", + purpose: "maskable", + type: "image/png", + }, + { + src: "/assets/icon/chrome-512.png", + sizes: "512x512", + type: "image/png", + }, + { + src: "/assets/icon/chrome-192.png", + sizes: "192x192", + type: "image/png", + }, + ], + }, + }, + }, +}); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..dbace82ba7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,99 @@ +--- +home: true +icon: home +title: 主页 +heroImage: http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/logo.png +heroText: Java程序员进阶之路 +tagline: 沉默王二BB:这是一份通俗易懂、风趣幽默的Java学习指南,内容涵盖Java基础、Java并发编程、Java虚拟机、Java企业级开发、Java面试等核心知识点。学Java,就认准Java程序员进阶之路😄 +actions: + - text: 立马上路→ + link: /home/ + type: primary + - text: 知识星球 + link: /zhishixingqiu/ + type: default +--- + +## 优质专栏 + +- **[《Java 面试指南》](/zhishixingqiu/java-mianshi-zhinan.md)** : 内容上与《Java 程序员进阶之路》形成互补,助力你快速成长成为 Offer 收割机! +- **[《亿点点小请求》](https://github.com/itwanger/toBeBetterJavaer)** : 建议戳链接给知识库点个 star,满足一下我的虚荣心,内容质量也绝对值得你一个 star。我还在继续创作,给我一点继续更新的动力,笔芯。 + + +## 推荐阅读 + +- [CS 学习指南👉](/xuexiluxian/) : 一份涵盖 Java、C 语言、C++、Python、Go、前端、操作系统、蓝桥杯、算法和数据结构的全方位 CS 学习路线!清晰且有效! +- [Java程序员常读书单📚,附下载地址](https://gitee.com/itwanger/JavaBooks) : 助力Java 程序员构建最强知识体系,涵盖Java、设计模式、数据库、数据结构与算法、大数据、架构等等,再也不用辛苦去找下载地址了。 +- [编程喵喵🐱实战项目学习教程](https://github.com/itwanger/codingmore-learning) :codingmore(Spring Boot+Vue 的前后端分离项目,一款值得一试的开源知识库学习网站)的学习教程,需要项目经验的 Java 开发者必备! +- [面渣逆袭📗](sidebar/sanfene/nixi.md) :面试前必刷,硬核理解版八股文,包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis 等等,助你拿到心仪 offer! + + +## PDF + +- [👏下载→Java程序员常读书单](download/java.md) +- [👏下载→最全最硬核的Java面试 “备战” 资料](https://mp.weixin.qq.com/s/US5nTxbC2nYc1hWpn5Bozw) +- [👏下载→深入浅出Java多线程](https://mp.weixin.qq.com/s/pxKrjw_5NTdZfHOKCkwn8w) +- [👏下载→GitHub星标115k+的Java教程](https://mp.weixin.qq.com/s/d7Z0QoChNuP9bTwAGh2QCw) +- [👏下载→重学Java设计模式](https://mp.weixin.qq.com/s/PH5AizUAnTz0CuvJclpAKw) +- [👏下载→Java版LeetCode刷题笔记](https://mp.weixin.qq.com/s/FyoOPIMGcaeH0z5RMhxtaQ) +- [👏下载→阮一峰C语言入门教程](download/yuanyifeng-c-language.md) +- [👏下载→BAT大佬的刷题笔记](download/bat-shuati.md) +- [👏下载→给操作系统捋条线](https://mp.weixin.qq.com/s/puTGbgU7xQnRcvz5hxGBHA) +- [👏下载→豆瓣9.1分,Pro Git中文版](download/progit.md) +- [👏下载→简历模板](download/jianli.md) + +## 公众号 + +强烈推荐大家关注一波作者的原创公众号,专注于分享硬核的 Java 后端技术文章,但又能保证你在阅读的时候轻松惬意。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + +## star趋势图 + +[![Star History Chart](https://api.star-history.com/svg?repos=itwanger/toBeBetterJavaer&type=Date)](https://star-history.com/#itwanger/toBeBetterJavaer&Date) + + +## 友情链接 + +- [Hippo4J](https://github.com/acmenlt/dynamic-threadpool),🔥 强大的动态线程池,附带监控报警功能(没有依赖中间件),完全遵循阿里巴巴编码规范。 +- [JavaGuide](https://github.com/Snailclimb/JavaGuide),「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide! + +## 捐赠鼓励 + +开源不易,如果《Java 程序员进阶之路》对你有些帮助,可以请作者喝杯咖啡,算是对开源做出的一点点鼓励吧! + +
+ +
+ +:gift_heart: 感谢大家对我资金的赞赏,每隔一个月会统计一次。 + +时间|小伙伴|赞赏金额 +---|---|--- +2022-05-12|*烟|10元 +2022-04-25|*思|5元 +2022-04-20|w*n|1元 +2022-04-12|*生|10元 +2022-03-04|袁*波|99元 +2022-02-17|*色|1元 +2022-02-17|M*y|1元 +2022-01-28|G*R|6.6元 +2022-01-20|*光|50元 +2022-01-14|*浩|1元 +2022-01-01|马*谊|6.6元 +2022-01-01|刚*好|3.6元 +2021-12-20|t*1|5 元 +2021-10-26|*鱼|28 元 +2021-10-11|*人|28 元 +2021-09-01|S*n|6.6 元 +2021-08-02|*秒|1 元 +2021-06-13|*7| 28 元 +2021-04-29|p*e|2 元 + + + +## 参与贡献 + +1. 如果你对本项目有任何建议或发现文中内容有误的,欢迎提交 issues 进行指正。 +2. 对于文中我没有涉及到知识点,欢迎提交 PR。 \ No newline at end of file diff --git a/docs/src/about-the-author/bzhan-10wan.md b/docs/about-the-author/bzhan-10wan.md similarity index 89% rename from docs/src/about-the-author/bzhan-10wan.md rename to docs/about-the-author/bzhan-10wan.md index 503848d6d3..23b0508a67 100644 --- a/docs/src/about-the-author/bzhan-10wan.md +++ b/docs/about-the-author/bzhan-10wan.md @@ -5,18 +5,19 @@ tag: - 心路历程 --- + # 我的第一个,10 万(B站视频播放) 恭喜这个 B。。。。。。站上的 UP,上一期视频播放量突破了 10 万!这也是二哥人生当中的第一次,凭借单条视频突破 10 万播放,必须得纪念下。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-4f27a848-7dba-4cd3-a705-a6ef02162338.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-4f27a848-7dba-4cd3-a705-a6ef02162338.png) 从众多的宫斗剧中我得出了一条宝贵的人生经验:“母凭子贵”。这条经验同样适用于二哥本人,可能会因为这一期视频,吹这辈子最多的牛逼:这不,荣获哔哩哔哩第 3 周【校园优秀奖&校园新星奖】。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-340f1fe4-49f1-48c6-a972-c5da7de8ddd0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-340f1fe4-49f1-48c6-a972-c5da7de8ddd0.png) 我已经按捺不住激动的心情,在两万人的朋友圈大肆炫耀了。十万播放,对于百大 UP 来说,可能就是分分钟的事,可对于我这个(未来的) B站百大来说,苦苦等了 149 天!!!!!!! @@ -36,13 +37,13 @@ tag: 三不三连没关系,有关系的是不三连可能会对不起二哥的肝,所以还是三连吧,哈哈哈哈,瞧瞧我们这该死的生物钟,起这么早。。。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-5a1d423e-8827-4a66-9197-4641ef0ecbaf.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-5a1d423e-8827-4a66-9197-4641ef0ecbaf.png) 接下来,上干货,我把这期 10万+ 播放的视频台本重新整理了一下,本来不想发的,很多小伙伴私信说二哥偏爱 B 站,同步都懒得同步了吗? 这不,赶紧发到公众号上来,希望学生党们现在立刻马上收藏起来,这个寒假你会过得非常充实;至于工作党嘛,像二哥这样的,既要工作,又要读书写作照顾家庭的,忙都忙死了,就算了吧! -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-99073995-10b0-42ee-81e5-cc4abc26aa71.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-99073995-10b0-42ee-81e5-cc4abc26aa71.png) 啊,不,还是要稍微卷一卷吧,免得被那群还有半年就毕业的家伙们拍死在沙滩上。。。。 @@ -60,7 +61,7 @@ tag: 所以我的建议是,**趁寒假打打王者上上分吧**! -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-63412a28-7315-4ac4-a04b-a049b338c0d8.gif) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-63412a28-7315-4ac4-a04b-a049b338c0d8.gif) 啊,不!**趁寒假刷一波清华在 GitHub 上 20k+ star 的开源课程吧**! @@ -70,7 +71,7 @@ tag: >地址:https://github.com/PKUanonym/REKCARC-TSC-UHT -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-c309fdb8-084c-44b1-bd8e-86a4b39cbb7b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-c309fdb8-084c-44b1-bd8e-86a4b39cbb7b.png) 我来带小伙伴们过一下清华的课程安排哈,主要是针对计算机专业的。 @@ -105,7 +106,7 @@ tag: 学完这些,大家至少能学会下面这幅思维导图中列出来的内容。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-03e4f5b4-c756-401c-aada-695b9cfaf00d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-03e4f5b4-c756-401c-aada-695b9cfaf00d.png) 更多 C 语言的学习内容,可以戳下面这个链接,之前在公众号上发过了,这里就不再复制粘贴了: @@ -190,7 +191,7 @@ class 二哥 { 第一本,《趣学数据结构》 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-bd02be2f-ae71-413f-b0c0-0050fee0e2b5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-bd02be2f-ae71-413f-b0c0-0050fee0e2b5.png) 说到这,多说一嘴。2018 年的时候,人民邮电出版社的张老师邀请我出一本 Java 方面的书,我当时想命名为《趣学 Java》。张老师说,刚好之前和陈小玉老师合作出了一本《趣学算法》的书,要不发一本你看看吧。 @@ -198,11 +199,11 @@ class 二哥 { 第二本,《数据结构(C++语言版)》 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-de28bb8a-ddb6-4b73-b132-ebf5e507fdbe.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-de28bb8a-ddb6-4b73-b132-ebf5e507fdbe.png) 对,清华大学邓俊辉教授编著的,豆瓣评分也蛮高的。这本书还配套了视频课程,是免费的,可以在学堂在线上看,我之前也有推荐过。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-15de0615-3d9c-4bdc-bedb-4165ac6f4802.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-15de0615-3d9c-4bdc-bedb-4165ac6f4802.png) 课程质量木得说,算是国家级精品课了。大家有时间的话,一定要刷一遍。 @@ -217,13 +218,13 @@ class 二哥 { 第一本,《数据结构与算法分析(Java 语言描述)》 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-1672e79d-a576-4a24-bc60-ced47b692a0f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-1672e79d-a576-4a24-bc60-ced47b692a0f.png) 虽然翻译得不怎么样,但内容很全面,适合拿来作为一本数据结构的入门书。 第二本,《算法(第 4 版)》 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-9e260fc8-69fa-4dfa-82c6-9d6b1d7027e5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/bzhan-10wan-9e260fc8-69fa-4dfa-82c6-9d6b1d7027e5.png) 虽然名为算法,但大家都知道,算法是基于数据结构的,数组、队列、栈、堆、二叉树、哈希表等等,这些数据结构都讲到了。 diff --git a/docs/src/about-the-author/csdn-1000wan.md b/docs/about-the-author/csdn-1000wan.md similarity index 77% rename from docs/src/about-the-author/csdn-1000wan.md rename to docs/about-the-author/csdn-1000wan.md index fce9224f7b..5802693f65 100644 --- a/docs/src/about-the-author/csdn-1000wan.md +++ b/docs/about-the-author/csdn-1000wan.md @@ -14,15 +14,15 @@ tag: 我努力的回想着,回想自己在 2021 年做出了哪些耀眼的成绩,正襟危坐,回想良久,也只想到这最后一件:**CSDN 的博文访问量也突破了一千万**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-2627c2a4-46c1-49a9-b86f-607c65ba6398.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-2627c2a4-46c1-49a9-b86f-607c65ba6398.png) 但这算不算得上是成绩,很难说,因为喜欢这个平台的人有很多,不喜欢这个平台的也有很多。也许,GitHub 上有 110k+ star 的 JavaGuide 的话最具有说服力了,这个平台不规范转载的很多,垃圾资源下载的很多,但也有几个优秀的作者撑起了 CSDN 的半边天,二哥就是其中一个。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-a7c74449-7183-453f-8286-92d0bfe0a56d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-a7c74449-7183-453f-8286-92d0bfe0a56d.png) 老读者都知道,我是从2014 年,开始坚持写技术博客的。一开始,还没敢在 CSDN 上写,只敢在 JavaEye 上写(估计很多新读者都不太知道这个平台)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-43677bfa-bdd6-4c3b-aa67-3a3277dc3575.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-43677bfa-bdd6-4c3b-aa67-3a3277dc3575.png) 那时候的 JavaEye 真的是非常非常非常的纯粹(比博客园更纯粹),没有任何商业广告,还时不时送送书,头部作者有 fastjson 的作者温少,《亿级流量网站架构核心技术》作者开涛,想必做技术的大家应该都知道他们俩。 @@ -30,13 +30,13 @@ tag: 就这样写着写着,我成了 CSDN 的博客专家,出版了一本技术图书,成为了两届博客之星。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-5137a245-035e-4e36-ba04-2b06fae275e1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-5137a245-035e-4e36-ba04-2b06fae275e1.png) 就这样写着写着,我遇到了越来越多的读者,给他们提供帮助的同时,也成为了他们前进的动力。 据我自己的不完全统计,2021 年,我在朋友圈和公众号送出去了超过 200 本技术图书,每次我都会留个小心机,问中奖的读者是怎么认识二哥的,有没有什么建议,留言中竟然很多都来自 CSDN,这让我又惊又喜。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-11df1e5d-2505-416c-949a-607b4ebbc61f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-11df1e5d-2505-416c-949a-607b4ebbc61f.png) 经常有读者夸赞二哥好有写作的天赋啊,其实哪里是有天赋,纯粹是因为写得多,所以才写得好。我现在的文笔,讲真,还不如上高中那会,那会才是真的笔下生花,诗都能写得出来,情书就更不用说了。 @@ -50,47 +50,47 @@ tag: 2 月 15 日,我和奶奶的合影。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-14d7737b-7324-4717-bd16-a3da7f2b1223.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-14d7737b-7324-4717-bd16-a3da7f2b1223.png) 3 月 26 日,读者考上研究生了,特意发来祝贺。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-63626ad5-8580-4565-a0da-af3a0bf43875.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-63626ad5-8580-4565-a0da-af3a0bf43875.png) 4 月 3 日,和教练小姐姐在健身房合影。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-eb256e24-de8a-49d0-a6ef-eb6d0b8d28e7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-eb256e24-de8a-49d0-a6ef-eb6d0b8d28e7.png) 5 月 25 日,二哥的读者群体扩大了台湾省。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-b2a8a63c-8c8f-4d39-9a9f-acf54c908058.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-b2a8a63c-8c8f-4d39-9a9f-acf54c908058.png) 6 月 13 日,和家人畅游青岛。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-b430951d-544c-4743-b700-3c71a854267a.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-b430951d-544c-4743-b700-3c71a854267a.png) 7 月 20 日,被某某女粉追着要联系方式。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-d7e127c5-5560-45c0-87c8-4cea924047cd.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-d7e127c5-5560-45c0-87c8-4cea924047cd.png) 8 月 21 日,在十八线县城的老家砸核桃吃。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-944b3f88-86ab-4e86-aeb6-4ad1f4f1ece3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-944b3f88-86ab-4e86-aeb6-4ad1f4f1ece3.png) 9 月 23 日,收到掘金和 CSDN 寄来的月饼。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-0056fcd2-afa4-475e-ad69-95b84b90b8c7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-0056fcd2-afa4-475e-ad69-95b84b90b8c7.png) -10 月 11 日,收到《二哥的Java进阶之路》专栏在 GitHub 上开源以来的两笔大额打赏。 +10 月 11 日,收到《Java 程序员进阶之路》专栏在 GitHub 上开源以来的两笔大额打赏。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-7e6d9918-6d94-45ef-9afa-42882f79944d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-7e6d9918-6d94-45ef-9afa-42882f79944d.png) 11 月 6 日,和四位河科大的学弟撸完串后在校园里的合影。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-0c1b3624-f4e5-4e0c-b119-0ef4d440a60d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-0c1b3624-f4e5-4e0c-b119-0ef4d440a60d.png) 12 月 27 日,CSDN 生成的年度报告。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-a070af45-bfeb-4b1a-9b29-745279a4a0fc.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-a070af45-bfeb-4b1a-9b29-745279a4a0fc.png) 不知道大家的 2021 过得怎么样? @@ -107,7 +107,7 @@ tag: 一年时间过得可真快,有很多想做好的事情,到最后都差了点意思。就说一件吧,B 站的视频播放量没有达到预期。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-83e7cfbc-496d-45e8-bfd6-cd0d1dfc60d3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-83e7cfbc-496d-45e8-bfd6-cd0d1dfc60d3.png) 8 月份还能坚持一周输出一个,从一开始面对镜头时的恐惧,到慢慢接纳自己。但好景不长,9 月份的时候,视频播放量呈现下降趋势,我就开始胡思乱想了。 @@ -159,21 +159,21 @@ B 站我一定做到一万粉——这个 flag 不能到。 展望 2022 年,有太多的期待了。 -这不,新的惊喜就是《二哥的Java进阶之路》专栏第一次上了 GitHub 的 trending 榜单! +这不,新的惊喜就是《Java 程序员进阶之路》专栏第一次上了 GitHub 的 trending 榜单! -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-8868c50f-d622-4f1f-a92b-13cf95edd786.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-8868c50f-d622-4f1f-a92b-13cf95edd786.png) 正应了那句话,功夫不负有心人。 对于这个开源专栏,我投入了大量的心血,一开始的名字叫《教妹学 Java》,主打 Java 的入门级路线,一直连载了近 100篇原创内容。 -后来有朋友建议我,要想走国际化的话,就必须得换个名字,得和国际接轨,于是我就想破脑袋,征求了很多朋友的建议,改成这个《二哥的Java进阶之路》了,因为我之前出版过一本技术书《Web 全栈开发进阶之路》,叫这个名字刚好也非常适合。 +后来有朋友建议我,要想走国际化的话,就必须得换个名字,得和国际接轨,于是我就想破脑袋,征求了很多朋友的建议,改成这个《Java 程序员进阶之路》了,因为我之前出版过一本技术书《Web 全栈开发进阶之路》,叫这个名字刚好也非常适合。 英文名字叫 toBeBetterJavaer, 前后呼应,一气呵成。 立个 flag 吧,**2022 年,冲 5000 star**! -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-5ebc2c65-f342-45f9-aaf6-7f663b5406b8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/csdn-1000wan-5ebc2c65-f342-45f9-aaf6-7f663b5406b8.png) 这个 flag 绝不能倒! diff --git a/docs/about-the-author/readme.md b/docs/about-the-author/readme.md new file mode 100644 index 0000000000..e7e81dcd03 --- /dev/null +++ b/docs/about-the-author/readme.md @@ -0,0 +1,105 @@ +--- +title: 个人介绍 Q&A +category: 联系作者 +--- + +大家好,我是二哥呀!这篇文章会通过 QA 的形式简单介绍一下我自己。 + +## 一、我取得了哪些成绩? + +又到了晒成绩的环节,真让人迫不及待啊(瞧我这该死的自信)! + +### 01、公众号 + +目前我的原创公众号“**沉默王二**”有 10.5 万+ 读者关注,专注于分享硬核的 Java 后端技术文章。平均阅读 5500 左右,综合排名能排在全国开发者中的前 50 名左右(数据来源于二十次幂)。 + +可以微信搜索 **沉默王二** 关键字或者扫码直接关注,关注后回复 **00** 还可以拉取我为你精心准备的学习资料。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-34972eb2-f214-48db-a43e-c44918dfa23e.png) + +学习资料有 BAT 大佬的刷题笔记,有《Java 程序员进阶之路》的 PDF 版电子书等等。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-a3b81b80-03ec-470c-a9aa-ae8868e239cd.png) + + + +### 02、CSDN + +两届博客之星,总榜前 10 选手,访问量 1100 万+,粉丝 34 万+,妥妥的裆部博主,哦,不,头部博主。 + +>访问地址:https://blog.csdn.net/qing_gee + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-14fd83ec-db6e-4a6f-a8e9-8ce1ce0097c3.png) + +### 03、知乎 + +LV9 选手,阅读总数超 1590 万,今年卷一卷的话,破 2000 万阅读没什么问题。 + +>访问地址:https://www.zhihu.com/people/cmower + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-0fa19b6e-d06c-436b-bd11-1de8265c56bb.png) + +### 04、B 站 + +B 站还比较菜,目前只有一个 10 万+播放量的视频,等我的开源项目编程喵喵🐱开发完第一个版本后,开始重新卷视频。 + +>访问地址:https://space.bilibili.com/513340480 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-5db6c62f-6194-4022-aee5-daf4d1a19f0c.png) + +### 05、GitHub + +目前主要维护的《Java 程序员进阶之路》开源版在 GitHub 上有 1.8k+ 的 star,和出版社约定的是,超过 1 万 star 就出书,小伙伴们可以来点赞支持下。 + +>访问地址:https://github.com/itwanger/toBeBetterJavaer + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-aa477206-41a9-4c55-a649-3d87ba1cb26b.png) + + +### 05、知识星球 + +目前还处在试运营阶段,正在筹备星球用户专属的 5 份小册,质量高的一笔。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-c3dd1280-098e-460c-9a41-7d566976392b.png) + +内容涵盖实战项目开发笔记、面试指南、Java学习、LeetCode Java 版刷题笔记等优质内容,价值远超门票! + +- 编程喵喵开源 Spring Boot+Vue 的前后端分离项目实战笔记 +- **Java 面试指南**,今年重点更新内容,涵盖面试准备篇、技术面试题篇、面经篇、职场修炼篇等等硬核内容。 +- Java 程序员进阶之路优化重构版(星球专属) +- GitHub 上星标 147k+ 的 Java 教程(更多优质教程持续更新) +- LeetCode 题解 Java 版(持续更新 300 道) + +这是《Java 面试指南》专栏目前已经更新的内容,讲真,就这一个专栏就值回票价(新人优惠价只有 69 元)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-066ef990-a603-4ace-9a19-728eeb319924.png) + +还有星球内部我也在坚持每天更新优质的内容。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/about-the-author/readme-e108c929-ebc5-4d75-8d40-825f6d027117.png) + +喜欢的小伙伴可以直接扫码加入。 + +![](http://cdn.tobebetterjavaer.com/itwanger/zhishixingqiu-youhui30yuan.png) + +## 二、为什么叫沉默王二 + +其实原因很简单,我个人比较喜欢王小波,小波是个程序员,还是个作家,写的小说和杂文我都特别喜欢,有一本叫《沉默的大多数》,我就取了沉默二字,《黄金时代》里和陈清扬搞破鞋的男主就叫王二,加上小波在家排行老二,上面有个哥哥,下面有个弟弟,所以综合到一起就叫“沉默王二”了。 + +## 三、为什么做这个开源知识库 + +> [!NOTE] +> 知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 Java 程序员进阶之路😄。 +> +> 知识库旨在为学习 Java 的小伙伴提供一系列: +> - **优质的原创 Java 教程** +> - **全面清晰的 Java 学习路线** +> - **免费但靠谱的 Java 学习资料** +> - **精选的 Java 岗求职面试指南** +> - **Java 企业级开发所需的必备技术** +> +> 赠人玫瑰手有余香。知识库会持续保持**更新**,欢迎收藏品鉴! + +>访问地址:[https://tobebetterjavaer.com](https://tobebetterjavaer.com) + +## 四、未完待续 diff --git a/docs/src/about-the-author/zhihu-1000wan.md b/docs/about-the-author/zhihu-1000wan.md similarity index 88% rename from docs/src/about-the-author/zhihu-1000wan.md rename to docs/about-the-author/zhihu-1000wan.md index 4fc276dbde..6c3f4b845f 100644 --- a/docs/src/about-the-author/zhihu-1000wan.md +++ b/docs/about-the-author/zhihu-1000wan.md @@ -13,7 +13,7 @@ tag: 前几天,偷偷摸摸过了自己的第 N 个 18 岁,本来不想过生日的,就想当做是平常的一天。结果我妹非要提醒我,大家伙瞧瞧,这像妹妹该说的话吗? -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-5addb157-141f-400b-a51f-77557c8fdb8d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/zhihu-1000wan-5addb157-141f-400b-a51f-77557c8fdb8d.png) 呜呜呜~ @@ -23,7 +23,7 @@ tag: **经营了近一年的知乎,阅读总数突破了一千万,这也是我人生当中的第一个**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-0324afde-4009-4e80-b878-2311ff88e5ca.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/zhihu-1000wan-0324afde-4009-4e80-b878-2311ff88e5ca.png) 其实早在 11 月就破了千万,当时就想记录一下,但细想一下,好像这点成绩也算不上什么。毕竟逼乎上人均 985、年薪百万、刚下飞机的大佬多的是。 @@ -33,13 +33,13 @@ tag: 这不,前几天一个帖子莫名其妙被知乎删除了,我是无感知的。一个小伙伴为了看这个帖子,还特意发起了一次 9.8 元的付费咨询。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-2fdd5b2b-67c5-40cf-b0e4-0a92a37e659a.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/zhihu-1000wan-2fdd5b2b-67c5-40cf-b0e4-0a92a37e659a.png) 这足以说明这个帖子的内容是足够硬核的。 写知乎这近一年时间里,有一个帖子无声无息地爆了:**60 万+的阅读,7000+赞同,2.3 万+次的收藏**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-8b4637f2-08c9-479b-855f-3fd332d44651.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/zhihu-1000wan-8b4637f2-08c9-479b-855f-3fd332d44651.png) 不对啊,收藏竟然是点赞的 3 倍还多。。嗯,此时此刻天空飘出来了四个字:白票真香。 @@ -101,7 +101,7 @@ tag: 我在知乎上还有不少硬核输出,尤其是这些千赞以上的帖子,真心推荐给大家看看,看完后绝壁是有收获的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-4612a83f-6207-496c-b32b-c6f1ab031c4f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/szjy/zhihu-1000wan-4612a83f-6207-496c-b32b-c6f1ab031c4f.png) 虽然有些埋没的帖子我觉得价值也很高。不过,埋没就埋没吧。 diff --git a/docs/array/array.md b/docs/array/array.md new file mode 100644 index 0000000000..34644e884d --- /dev/null +++ b/docs/array/array.md @@ -0,0 +1,251 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 深入理解Java数组 + +“哥,我看你之前的文章里提到,ArrayList 的内部是用数组实现的,我就对数组非常感兴趣,想深入地了解一下,今天终于到这个环节了,好期待呀!”三妹的语气里显得很兴奋。 + +“的确是的,看 ArrayList 的源码就一清二楚了。”我一边说,一边打开 Intellij IDEA,并找到了 ArrayList 的源码。 + +```java +/** + * The array buffer into which the elements of the ArrayList are stored. + * The capacity of the ArrayList is the length of this array buffer. Any + * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA + * will be expanded to DEFAULT_CAPACITY when the first element is added. + */ +transient Object[] elementData; // non-private to simplify nested class access + +/** + * The size of the ArrayList (the number of elements it contains). + * + * @serial + */ +private int size; +``` + +“瞧见没?`Object[] elementData` 就是数组。”我指着显示屏上这串代码继续说。 + +数组是一个对象,它包含了一组固定数量的元素,并且这些元素的类型是相同的。数组会按照索引的方式将元素放在指定的位置上,意味着我们可以通过索引来访问这些元素。在 Java 中,索引是从 0 开始的。 + +“哥,能说一下为什么索引从 0 开始吗?”三妹突然这个话题很感兴趣。 + +“哦,Java 是基于 C/C++ 语言实现的,而 C 语言的下标是从 0 开始的,所以 Java 就继承了这个良好的传统习惯。C语言有一个很重要概念,叫做指针,它实际上是一个偏移量,距离开始位置的偏移量,第一个元素就在开始的位置,它的偏移量就为 0,所以索引就为 0。”此刻,我很自信。 + +“此外,还有另外一种说法。早期的计算机资源比较匮乏,0 作为起始下标相比较于 1 作为起始下标,编译的效率更高。” + +“哦。”三妹意味深长地点了点头。 + +我们可以将数组理解为一个个整齐排列的单元格,每个单元格里面存放着一个元素。 + +数组元素的类型可以是基本数据类型(比如说 int、double),也可以是引用数据类型(比如说 String),包括自定义类型。 + +数组的声明方式分两种。 + +先来看第一种: + +```java +int[] anArray; +``` + +再来看第二种: + +```java +int anOtherArray[]; +``` + +不同之处就在于中括号的位置,是跟在类型关键字的后面,还是跟在变量的名称的后面。前一种的使用频率更高一些,像 ArrayList 的源码中就用了第一种方式。 + +同样的,数组的初始化方式也有多种,最常见的是: + +```java +int[] anArray = new int[10]; +``` + +看到了没?上面这行代码中使用了 new 关键字,这就意味着数组的确是一个对象,只有对象的创建才会用到 new 关键字,基本数据类型是不用的。然后,我们需要在方括号中指定数组的长度。 + +这时候,数组中的每个元素都会被初始化为默认值,int 类型的就为 0,Object 类型的就为 null。 不同数据类型的默认值不同,可以参照[之前的文章](https://mp.weixin.qq.com/s/twim3w_dp5ctCigjLGIbFw)。 + +另外,还可以使用大括号的方式,直接初始化数组中的元素: + +```java +int anOtherArray[] = new int[] {1, 2, 3, 4, 5}; +``` + +这时候,数组的元素分别是 1、2、3、4、5,索引依次是 0、1、2、3、4,长度是 5。 + +“哥,怎么访问数组呢?”三妹及时地插话到。 + +前面提到过,可以通过索引来访问数组的元素,就像下面这样: + +```java +anArray[0] = 10; +``` + +变量名,加上中括号,加上元素的索引,就可以访问到数组,通过“=”操作符可以对元素进行赋值。 + +如果索引的值超出了数组的界限,就会抛出 `ArrayIndexOutOfBoundException`。 + +既然数组的索引是从 0 开始,那就是到数组的 `length - 1` 结束,不要使用超出这个范围内的索引访问数组,就不会抛出数组越界的异常了。 + +当数组的元素非常多的时候,逐个访问数组就太辛苦了,所以需要通过遍历的方式。 + +第一种,使用 for 循环: + +```java +int anOtherArray[] = new int[] {1, 2, 3, 4, 5}; +for (int i = 0; i < anOtherArray.length; i++) { + System.out.println(anOtherArray[i]); +} +``` + +通过 length 属性获取到数组的长度,然后从 0 开始遍历,就得到了数组的所有元素。 + +第二种,使用 for-each 循环: + +```java +for (int element : anOtherArray) { + System.out.println(element); +} +``` + +如果不需要关心索引的话(意味着不需要修改数组的某个元素),使用 for-each 遍历更简洁一些。当然,也可以使用 while 和 do-while 循环。 + +在 Java 中,可变参数用于将任意数量的参数传递给方法,来看 `varargsMethod()` 方法: + +```java +void varargsMethod(String... varargs) {} +``` + +该方法可以接收任意数量的字符串参数,可以是 0 个或者 N 个,本质上,可变参数就是通过数组实现的。为了证明这一点,我们可以看一下反编译一后的字节码: + +```java +public class VarargsDemo +{ + + public VarargsDemo() + { + } + + transient void varargsMethod(String as[]) + { + } +} +``` + +所以,我们其实可以直接将数组作为参数传递给该方法: + +```java +VarargsDemo demo = new VarargsDemo(); +String[] anArray = new String[] {"沉默王二", "一枚有趣的程序员"}; +demo.varargsMethod(anArray); +``` + +也可以直接传递多个字符串,通过逗号隔开的方式: + +```java +demo.varargsMethod("沉默王二", "一枚有趣的程序员"); +``` + +在 Java 中,数组与 List 关系非常密切。List 封装了很多常用的方法,方便我们对集合进行一些操作,而如果直接操作数组的话,有很多不便,因为数组本身没有提供这些封装好的操作,所以有时候我们需要把数组转成 List。 + +“怎么转呢?”三妹问到。 + +最原始的方式,就是通过遍历数组的方式,一个个将数组添加到 List 中。 + +```java +int[] anArray = new int[] {1, 2, 3, 4, 5}; + +List aList = new ArrayList<>(); +for (int element : anArray) { + aList.add(element); +} +``` + +更优雅的方式是通过 Arrays 类的 `asList()` 方法: + +```java +List aList = Arrays.asList(anArray); +``` + +但需要注意的是,该方法返回的 ArrayList 并不是 `java.util.ArrayList`,它其实是 Arrays 类的一个内部类: + +```java +private static class ArrayList extends AbstractList + implements RandomAccess, java.io.Serializable{} +``` + +如果需要添加元素或者删除元素的话,需要把它转成 `java.util.ArrayList`。 + +```java +new ArrayList<>(Arrays.asList(anArray)); +``` + +Java 8 新增了 Stream 流的概念,这就意味着我们也可以将数组转成 Stream 进行操作。 + +```java +String[] anArray = new String[] {"沉默王二", "一枚有趣的程序员", "好好珍重他"}; +Stream aStream = Arrays.stream(anArray); +``` + + +如果想对数组进行排序的话,可以使用 Arrays 类提供的 `sort()` 方法。 + +- 基本数据类型按照升序排列 +- 实现了 Comparable 接口的对象按照 `compareTo()` 的排序 + +来看第一个例子: + +```java +int[] anArray = new int[] {5, 2, 1, 4, 8}; +Arrays.sort(anArray); +``` + +排序后的结果如下所示: + +```java +[1, 2, 4, 5, 8] +``` + +来看第二个例子: + +```java +String[] yetAnotherArray = new String[] {"A", "E", "Z", "B", "C"}; +Arrays.sort(yetAnotherArray, 1, 3, + Comparator.comparing(String::toString).reversed()); +``` + +只对 1-3 位置上的元素进行反序,所以结果如下所示: + +``` +[A, Z, E, B, C] +``` + +有时候,我们需要从数组中查找某个具体的元素,最直接的方式就是通过遍历的方式: + +```java +int[] anArray = new int[] {5, 2, 1, 4, 8}; +for (int i = 0; i < anArray.length; i++) { + if (anArray[i] == 4) { + System.out.println("找到了 " + i); + break; + } +} +``` + +上例中从数组中查询元素 4,找到后通过 break 关键字退出循环。 + +如果数组提前进行了排序,就可以使用二分查找法,这样效率就会更高一些。`Arrays.binarySearch()` 方法可供我们使用,它需要传递一个数组,和要查找的元素。 + +```java +int[] anArray = new int[] {1, 2, 3, 4, 5}; +int index = Arrays.binarySearch(anArray, 4); +``` + +“除了一维数组,还有二维数组,三妹你可以去研究下,比如说用二维数组打印一下杨辉三角。”说完,我就去阳台上休息了,留三妹在那里学习,不能打扰她。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/array/print.md b/docs/array/print.md new file mode 100644 index 0000000000..d6226c5568 --- /dev/null +++ b/docs/array/print.md @@ -0,0 +1,161 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 如何优雅地打印Java数组? + +“哥,之前听你说,数组也是一个对象,但 Java 中并未明确的定义这样一个类。”看来三妹有在用心地学习。 + +“是的,因此数组也就没有机会覆盖 `Object.toString()` 方法。如果尝试直接打印数组的话,输出的结果并不是我们预期的结果。”我接着三妹的话继续说。 + +“那怎么打印数组呢?”三妹心有灵犀地把今天的核心问题提了出来。 + +“首先,我们来看一下,为什么不能直接打印数组,直接打印的话,会出现什么问题。” + +来看这样一个例子。 + +``` +String [] cmowers = {"沉默","王二","一枚有趣的程序员"}; +System.out.println(cmowers); +``` + +程序打印的结果是: + +``` +[Ljava.lang.String;@3d075dc0 +``` + +`[Ljava.lang.String;` 表示字符串数组的 Class 名,@ 后面的是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 `java.lang.Object` 类的 `toString()` 方法就明白了。 + +```java +public String toString() { + return getClass().getName() + "@" + Integer.toHexString(hashCode()); +} +``` + +再次证明,数组虽然没有显式定义成一个类,但它的确是一个对象,继承了祖先类 Object 的所有方法。 + +“哥,那为什么数组不单独定义一个类来表示呢?就像字符串 String 类那样呢?”三妹这个问题让人头大,但也好解释。 + +“一个合理的说法是 Java 将其隐藏了。假如真的存在这么一个类,就叫 Array.java 吧,我们假想一下它真实的样子,必须得有一个容器来存放数组的每一个元素,就像 String 类那样。”一边回答三妹,我一边打开了 String 类的源码。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; +} +``` + +“最终还是要用类似一种数组的形式来存放数组的元素,对吧?这就变得很没有必要了,不妨就把数组当做是一个没有形体的对象吧!” + +“好了,不讨论这个了。”我怕话题扯远了,扯到我自己也答不出来就尴尬了,赶紧把三妹的思路拽了回来。 + +“我们来看第一种打印数组的方法,使用时髦一点的 Stream 流。” + +第一种形式: + +```java +Arrays.asList(cmowers).stream().forEach(s -> System.out.println(s)); +``` + +第二种形式: + +```java +Stream.of(cmowers).forEach(System.out::println); +``` + +第三种形式: + +```java +Arrays.stream(cmowers).forEach(System.out::println); +``` + +打印的结果如下所示。 + +``` +沉默 +王二 +一枚有趣的程序员 +``` + +没错,这三种方式都可以轻松胜任本职工作,并且显得有点高大上,毕竟用到了 Stream,以及 lambda 表达式。 + +“当然了,也可以使用比较土的方式,for 循环。甚至 for-each 也行。” + +```java +for(int i = 0; i < cmowers.length; i++){ + System.out.println(cmowers[i]); +} + +for (String s : cmowers) { + System.out.println(s); +} +``` + +“哥,你难道忘了[上一篇](https://mp.weixin.qq.com/s/acnDNH6A8USm_EYIT6i-jA)在讲 Arrays 工具类的时候,提到过另外一种方法 `Arrays.toString()` 吗?”三妹看我一直说不到点子上,有点着急了。 + +“当然没有了,我认为 `Arrays.toString()` 是打印数组的最佳方式,没有之一。”我的情绪有点激动。 + +`Arrays.toString()` 可以将任意类型的数组转成字符串,包括基本类型数组和引用类型数组。该方法有多种重载形式。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/array/print-01.png) + +使用 `Arrays.toString()` 方法来打印数组再优雅不过了,就像,就像,就像蒙娜丽莎的微笑。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/array/print-02.png) + +(三妹看到这么一副图的时候忍不住地笑了) + +“三妹,你不要笑,来,怀揣着愉快的心情看一下代码示例。” + +```java +String [] cmowers = {"沉默","王二","一枚有趣的程序员"}; +System.out.println(Arrays.toString(cmowers)); +``` + +程序打印结果: + +``` +[沉默, 王二, 一枚有趣的程序员] +``` + +哇,打印格式不要太完美,不多不少!完全是我们预期的结果:`[]` 表明是一个数组,`,` 点和空格用来分割元素。 + +“哥,那如果我想打印二维数组呢?” + +“可以使用 `Arrays.deepToString()` 方法。” + +```java +String[][] deepArray = new String[][] {{"沉默", "王二"}, {"一枚有趣的程序员"}}; +System.out.println(Arrays.deepToString(deepArray)); +``` + +打印结果如下所示。 + +``` +[[沉默, 王二], [一枚有趣的程序员]] +``` + +------- + +“说到打印,三妹,哥给你提醒一点。阿里巴巴的 Java 开发手册上有这样一条规约,你看。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/array/print-03.png) + +“什么是 POJO 呢,就是 Plain Ordinary Java Object 的缩写,一般在 Web 应用程序中建立一个数据库的映射对象时,我们称它为 POJO,这类对象不继承或不实现任何其它 Java 框架的类或接口。” + +“对于这样的类,最好是重写一下它的 `toString()` 方法,方便查看这个对象到底包含了什么字段,好排查问题。” + +“如果不重写的话,打印出来的 Java 对象就像直接打印数组的那样,一串谁也看不懂的字符序列。” + +“可以借助 Intellij IDEA 生成重写的 `toString()` 方法,特别方便。” + +“好的,哥,我记住了。以后遇到的话,我注意下。你去休息吧,我来敲一下你提到的这些代码,练一练。” + +“OK,我走,我走。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/baguwen/java-basic-34.md b/docs/baguwen/java-basic-34.md new file mode 100644 index 0000000000..5348b552fc --- /dev/null +++ b/docs/baguwen/java-basic-34.md @@ -0,0 +1,574 @@ +--- +category: + - 求职面试 +tag: + - 面试题集合 +--- + +# Java:34道精选高频面试题必看:+1: + +## 1.介绍一下 java 吧 + +java 是一门**开源的跨平台的面向对象的**计算机语言. + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-01.png) + +跨平台是因为 java 的 class 文件是运行在虚拟机上的,其实跨平台的,而**虚拟机是不同平台有不同版本**,所以说 java 是跨平台的. + +面向对象有几个特点: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-02.png) + +- 1.**封装** + - 两层含义:一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者'封装'在一个不可分割的**独立单元**(即对象)中 + - 另一层含义指'信息隐藏,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性或行为,则不允许外界知晓,或只允许使用对象的功能,而尽可能**隐藏对象的功能实现细节**。 + +**优点**: + +> 1.良好的封装能够**减少耦合**,符合程序设计追求'高内聚,低耦合'。
+> 2.**类内部的结构可以自由修改**。
+> 3.可以对成员变量进行更**精确的控制**。
+> 4.**隐藏信息**实现细节。
+ + +- 2.**继承** + - 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 + +**优点**: + +> 1.提高类代码的**复用性**
+> 2.提高了代码的**维护性**
+ +- 3.**多态** + - 多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态: + - 1.**方法重载**:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同。 + - 2.**对象多态**:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。 + + **优点** + +> 1. **消除类型之间的耦合关系**
+> 2. **可替换性**
+> 3. **可扩充性**
+> 4. **接口性**
+> 5. **灵活性**
+> 6. **简化性**
+ +## 2.java 有哪些数据类型? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-03.png) + +java 主要有两种数据类型 + + - 1.**基本数据类型** + - 基本数据有**八个**, + - byte,short,int,long属于数值型中的整数型 + - float,double属于数值型中的浮点型 + - char属于字符型 + - boolean属于布尔型 + - 2.**引用数据类型** + - 引用数据类型有**三个**,分别是类,接口和数组 + +## 3.接口和抽象类有什么区别? + +- 1.接口是抽象类的变体,**接口中所有的方法都是抽象的**。而抽象类是声明方法的存在而不去实现它的类。 +- 2.接口可以多继承,抽象类不行。 +- 3.接口定义方法,不能实现,默认是 **public abstract**,而抽象类可以实现部分方法。 +- 4.接口中基本数据类型为 **public static final** 并且需要给出初始值,而抽类象不是的。 + +## 4.重载和重写什么区别? + +重写: + +- 1.参数列表必须**完全与被重写的方法**相同,否则不能称其为重写而是重载. +- 2.**返回的类型必须一直与被重写的方法的返回类型相同**,否则不能称其为重写而是重载。 +- 3.访问**修饰符的限制一定要大于被重写方法的访问修饰符** +- 4.重写方法一定**不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常**。 + +重载: + +- 1.必须具有**不同的参数列表**; +- 2.可以有不同的返回类型,只要参数列表不同就可以了; +- 3.可以有**不同的访问修饰符**; +- 4.可以抛出**不同的异常**; + +## 5.常见的异常有哪些? + +- NullPointerException 空指针异常 +- ArrayIndexOutOfBoundsException 索引越界异常 +- InputFormatException 输入类型不匹配 +- SQLException SQL异常 +- IllegalArgumentException 非法参数 +- NumberFormatException 类型转换异常 + 等等.... + +## 6.异常要怎么解决? + +Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。 + +Throwable又派生出**Error类和Exception类**。 + +错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。 + +异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。 + +处理方法: + +- 1.**try()catch(){}** + +``` +try{ +// 程序代码 +}catch(ExceptionName e1){ +//Catch 块 +} +``` + +- 2.**throw** + - throw 关键字作用是抛出一个异常,抛出的时候是抛出的是一个异常类的实例化对象,在异常处理中,try 语句要捕获的是一个异常对象,那么此异常对象也可以自己抛出 +- 3.**throws** + - 定义一个方法的时候可以使用 throws 关键字声明。使用 throws 关键字声明的方法表示此方法不处理异常,而交给方法调用处进行处理。 + +## 7.arrayList 和 linkedList 的区别? + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-04.png) + +- 1.ArrayList 是实现了基于**数组**的,存储空间是连续的。LinkedList 基于**链表**的,存储空间是不连续的。(LinkedList 是双向链表) + +- 2.对于**随机访问** get 和 set ,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。 + +- 3.对于**新增和删除**操作 add 和 remove ,LinedList 比较占优势,因为 ArrayList 要移动数据。 + +- 4.同样的数据量 LinkedList 所占用空间可能会更小,因为 ArrayList 需要**预留空间**便于后续数据增加,而 LinkedList 增加数据只需要**增加一个节点** + +## 8.hashMap 1.7 和 hashMap 1.8 的区别? + +只记录**重点** + +| 不同点 | hashMap 1.7 | hashMap 1.8 | +| :-------------- | :----------------------------: | -----------------------------: | +| 数据结构 | 数组+链表 | 数组+链表+红黑树 | +| 插入数据的方式 | 头插法 | 尾插法 | +| hash 值计算方式 | 9次扰动处理(4次位运算+5次异或) | 2次扰动处理(1次位运算+1次异或) | +| 扩容策略 | 插入前扩容 | 插入后扩容 | + +## 9.hashMap 线程不安全体现在哪里? + +在 **hashMap1.7 中扩容**的时候,因为采用的是头插法,所以会可能会有循环链表产生,导致数据有问题,在 1.8 版本已修复,改为了尾插法 + +在任意版本的 hashMap 中,如果在**插入数据时多个线程命中了同一个槽**,可能会有数据覆盖的情况发生,导致线程不安全。 + +## 10.那么 hashMap 线程不安全怎么解决? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-05.png) + +- 一.给 hashMap **直接加锁**,来保证线程安全 +- 二.使用 **hashTable**,比方法一效率高,其实就是在其方法上加了 synchronized 锁 +- 三.使用 **concurrentHashMap** , 不管是其 1.7 还是 1.8 版本,本质都是**减小了锁的粒度,减少线程竞争**来保证高效. + +## 11.concurrentHashMap 1.7 和 1.8 有什么区别 + +只记录**重点** + +| 不同点 | concurrentHashMap 1.7 | concurrentHashMap 1.8 | +| :------- | :--------------------------: | ---------------------------------: | +| 锁粒度 | 基于segment | 基于entry节点 | +| 锁 | reentrantLock | synchronized | +| 底层结构 | Segment + HashEntry + Unsafe | Synchronized + CAS + Node + Unsafe | + +## 12.介绍一下 hashset 吧 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-06.png) + +上图是 set 家族整体的结构, + +set 继承于 Collection 接口,是一个**不允许出现重复元素,并且无序的集合**. + +HashSet 是**基于 HashMap 实现**的,底层**采用 HashMap 来保存元素** + +元素的哈希值是通过元素的 hashcode 方法 来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。 + +## 13.什么是泛型? + +泛型:**把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型** + +## 14.泛型擦除是什么? + +因为泛型其实只是在编译器中实现的而虚拟机并不认识泛型类项,所以要在虚拟机中将泛型类型进行擦除。也就是说,**在编译阶段使用泛型,运行阶段取消泛型,即擦除**。 擦除是将泛型类型以其父类代替,如String 变成了Object等。其实在使用的时候还是进行带强制类型的转化,只不过这是比较安全的转换,因为在编译阶段已经确保了数据的一致性。 + +## 15.说说进程和线程的区别? + +**进程是系统资源分配和调度的基本单位**,它能并发执行较高系统资源的利用率. + +**线程**是**比进程更小**的能独立运行的基本单位,创建、销毁、切换成本要小于进程,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。 + +## 16.volatile 有什么作用? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-07.png) + +- **1.保证内存可见性** + - 可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。 +- **2.禁止指令重排序** + - cpu 是和缓存做交互的,但是由于 cpu 运行效率太高,所以会不等待当前命令返回结果从而继续执行下一个命令,就会有乱序执行的情况发生 + +## 17.什么是包装类?为什么需要包装类? + +**Java 中有 8 个基本类型,分别对应的 8 个包装类** + +- byte -- Byte +- boolean -- Boolean +- short -- Short +- char -- Character +- int -- Integer +- long -- Long +- float -- Float +- double -- Double + +**为什么需要包装类**: + +- 基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持 +- 不符合面向对象思维 +- 包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等 + +## 18.Integer a = 1000,Integer b = 1000,a==b 的结果是什么?那如果 a,b 都为1,结果又是什么? + +Integer a = 1000,Integer b = 1000,a==b 结果为**false** + +Integer a = 1,Integer b = 1,a==b 结果为**true** + +这道题主要考察 Integer 包装类缓存的范围,**在-128~127之间会缓存起来**,比较的是直接缓存的数据,在此之外比较的是对象 + +## 19.JMM 是什么? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-08.png) + +JMM 就是 **Java内存模型**(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)**屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果**。 + +Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。**线程不能直接读写主内存中的变量**。 + +每个线程的工作内存都是独立的,**线程操作数据只能在工作内存中进行,然后刷回到主存**。这是 Java 内存模型定义的线程基本工作方式。 + + +## 20.创建对象有哪些方式 + +有**五种创建对象的方式** + +- 1、new关键字 + +``` +Person p1 = new Person(); +``` + +- 2.Class.newInstance + +``` +Person p1 = Person.class.newInstance(); +``` + +- 3.Constructor.newInstance + +``` +Constructor constructor = Person.class.getConstructor(); +Person p1 = constructor.newInstance(); +``` + +- 4.clone + +``` +Person p1 = new Person(); +Person p2 = p1.clone(); +``` + +- 5.反序列化 + +``` +Person p1 = new Person(); +byte[] bytes = SerializationUtils.serialize(p1); +Person p2 = (Person)SerializationUtils.deserialize(bytes); +``` + +## 21.讲讲单例模式懒汉式吧 + +直接贴代码 + +``` +// 懒汉式 +public class Singleton { +// 延迟加载保证多线程安全 + Private volatile static Singleton singleton; + private Singleton(){} + public static Singleton getInstance(){ + if(singleton == null){ + synchronized(Singleton.class){ + if(singleton == null){ + singleton = new Singleton(); + } + } + } + return singleton; + } +} +``` + +- 使用 volatile 是**防止指令重排序,保证对象可见**,防止读到半初始化状态的对象 +- 第一层if(singleton == null) 是为了防止有多个线程同时创建 +- synchronized 是加锁防止多个线程同时进入该方法创建对象 +- 第二层if(singleton == null) 是防止有多个线程同时等待锁,一个执行完了后面一个又继续执行的情况 + +[关于双检锁可以参考](https://blog.csdn.net/fly910905/article/details/79286680) + +## 22.volatile 有什么作用 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-09.png) + +- 1.**保证内存可见性** + - 当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量**写入数据**的时候,虚拟机会**强制它被值刷新到主内存中**。当一个线程**读取**被volatile关键字修饰的值的时候,虚拟机会**强制要求它从主内存中读取**。 +- 2.**禁止指令重排序** + - 指令重排序是编译器和处理器为了高效对程序进行优化的手段,cpu 是与内存交互的,而 cpu 的效率想比内存高很多,所以 cpu 会在不影响最终结果的情况下,不等待返回结果直接进行后续的指令操作,而 volatile 就是给相应代码加了**内存屏障**,在屏障内的代码禁止指令重排序。 + +## 23.怎么保证线程安全? + +- 1.synchronized关键字 + - 可以用于代码块,方法(静态方法,同步锁是当前字节码对象;实例方法,同步锁是实例对象) +- 2.lock锁机制 + +``` +Lock lock = new ReentrantLock(); +lock. lock(); +try { + System. out. println("获得锁"); +} catch (Exception e) { + +} finally { + System. out. println("释放锁"); + lock. unlock(); +} +``` + +## 24.synchronized 锁升级的过程 + +在 Java1.6 之前的版本中,synchronized 属于重量级锁,效率低下,**锁是** cpu 一个**总量级的资源**,每次获取锁都要和 cpu 申请,非常消耗性能。 + +在 **jdk1.6 之后** Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了,Jdk1.6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,**增加了锁升级的过程**,由无锁->偏向锁->自旋锁->重量级锁 +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-10.png) + +增加锁升级的过程主要是**减少用户态到核心态的切换,提高锁的效率,从 jvm 层面优化锁** + +## 25.cas 是什么? + +cas 叫做 CompareAndSwap,**比较并交换**,很多地方使用到了它,比如锁升级中自旋锁就有用到,主要是**通过处理器的指令来保证操作的原子性**,它主要包含三个变量: + +- **1.变量内存地址** +- **2.旧的预期值 A** +- **3.准备设置的新值 B** + +当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给 A,基于 A 进行计算,得到新值 B,在用预期原值 A 和内存中的共享变量值进行比较,**如果相同就认为其他线程没有进行修改**,而将新值写入内存 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-11.png) + +**CAS的缺点** + +- **CPU开销比较大**:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,又因为自旋的时候会一直占用CPU,如果CAS一直更新不成功就会一直占用,造成CPU的浪费。 + +- **ABA 问题**:比如线程 A 去修改 1 这个值,修改成功了,但是中间 线程 B 也修改了这个值,但是修改后的结果还是 1,所以不影响 A 的操作,这就会有问题。可以用**版本号**来解决这个问题。 + +- **只能保证一个共享变量的原子性** + +## 26.聊聊 ReentrantLock 吧 + +ReentrantLock 意为**可重入锁**,说起 ReentrantLock 就不得不说 AQS ,因为其底层就是**使用 AQS 去实现**的。 + +ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。 + +- 公平模式下等待线程入队列后会严格按照队列顺序去执行 +- 非公平模式下等待线程入队列后有可能会出现插队情况 + +**公平锁** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-12.png) + +- 第一步:**获取状态的 state 的值** + - 如果 state=0 即代表锁没有被其它线程占用,执行第二步。 + - 如果 state!=0 则代表锁正在被其它线程占用,执行第三步。 +- 第二步:**判断队列中是否有线程在排队等待** + - 如果不存在则直接将锁的所有者设置成当前线程,且更新状态 state 。 + - 如果存在就入队。 +- 第三步:**判断锁的所有者是不是当前线程** + - 如果是则更新状态 state 的值。 + - 如果不是,线程进入队列排队等待。 + +**非公平锁** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-13.png) + +- 获取状态的 state 的值 + - 如果 state=0 即代表锁没有被其它线程占用,则设置当前锁的持有者为当前线程,该操作用 CAS 完成。 + - 如果不为0或者设置失败,代表锁被占用进行下一步。 +- 此时**获取 state 的值** + - 如果是,则给state+1,获取锁 + - 如果不是,则进入队列等待 + - 如果是0,代表刚好线程释放了锁,此时将锁的持有者设为自己 + - 如果不是0,则查看线程持有者是不是自己 + +## 27.多线程的创建方式有哪些? + +- 1、**继承Thread类**,重写run()方法 + +``` +public class Demo extends Thread{ + //重写父类Thread的run() + public void run() { + } + public static void main(String[] args) { + Demo d1 = new Demo(); + Demo d2 = new Demo(); + d1.start(); + d2.start(); + } +} +``` + +- 2.**实现Runnable接口**,重写run() + +``` +public class Demo2 implements Runnable{ + + //重写Runnable接口的run() + public void run() { + } + + public static void main(String[] args) { + Thread t1 = new Thread(new Demo2()); + Thread t2 = new Thread(new Demo2()); + t1.start(); + t2.start(); + } + +} +``` + +- 3.**实现 Callable 接口** + +``` +public class Demo implements Callable{ + + public String call() throws Exception { + System.out.println("正在执行新建线程任务"); + Thread.sleep(2000); + return "结果"; + } + + public static void main(String[] args) throws InterruptedException, ExecutionException { + Demo d = new Demo(); + FutureTask task = new FutureTask<>(d); + Thread t = new Thread(task); + t.start(); + //获取任务执行后返回的结果 + String result = task.get(); + } + +} +``` + +- 4.**使用线程池创建** + +``` +public class Demo { + public static void main(String[] args) { + Executor threadPool = Executors.newFixedThreadPool(5); + for(int i = 0 ;i < 10 ; i++) { + threadPool.execute(new Runnable() { + public void run() { + //todo + } + }); + } + + } +} +``` + +## 28.线程池有哪些参数? + +- **1.corePoolSize**:**核心线程数**,线程池中始终存活的线程数。 +- **2.maximumPoolSize**: **最大线程数**,线程池中允许的最大线程数。 +- **3.keepAliveTime**: **存活时间**,线程没有任务执行时最多保持多久时间会终止。 + +- **4.unit**: **单位**,参数keepAliveTime的时间单位,7种可选。 +- **5.workQueue**: 一个**阻塞队列**,用来存储等待执行的任务,均为线程安全,7种可选。 +- **6.threadFactory**: **线程工厂**,主要用来创建线程,默及正常优先级、非守护线程。 + +- **7.handler**:**拒绝策略**,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。 + +## 29.线程池的执行流程? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-14.png) + +- 判断线程池中的线程数**是否大于设置的核心线程数** + - 如果**小于**,就**创建**一个核心线程来执行任务 + - 如果**大于**,就会**判断缓冲队列是否满了** + - 如果**没有满**,则**放入队列**,等待线程空闲时执行任务 + - 如果队列已经**满了**,则判断**是否达到了线程池设置的最大线程数** + - 如果**没有达到**,就**创建新线程**来执行任务 + - 如果已经**达到了**最大线程数,则**执行指定的拒绝策略** + +## 30.线程池的拒绝策略有哪些? + +- **AbortPolicy**:直接丢弃任务,抛出异常,这是默认策略 +- **CallerRunsPolicy**:只用调用者所在的线程来处理任务 +- **DiscardOldestPolicy**:丢弃等待队列中最旧的任务,并执行当前任务 +- **DiscardPolicy**:直接丢弃任务,也不抛出异常 + +## 31.介绍一下四种引用类型? + +- **强引用 StrongReference** + +``` +Object obj = new Object(); +//只要obj还指向Object对象,Object对象就不会被回收 +``` + +垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。 + +- **软引用 SoftReference** + +软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。 + +- **弱引用 WeakReference** + +弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。 + +- **虚引用 PhantomReference** + +虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理。 + +## 32.深拷贝、浅拷贝是什么? + +- 浅拷贝并不是真的拷贝,只是**复制指向某个对象的指针**,而不复制对象本身,新旧对象还是共享同一块内存。 +- 深拷贝会另外**创造一个一模一样的对象**,新对象跟原对象不共享内存,修改新对象不会改到原对象。 + +## 33.聊聊 ThreadLocal 吧 + +- ThreadLocal其实就是**线程本地变量**,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离。 +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-15.png) +- ThreadLocal 有一个**静态内部类 ThreadLocalMap**,ThreadLocalMap 又包含了一个 Entry 数组,**Entry 本身是一个弱引用**,他的 key 是指向 ThreadLocal 的弱引用,**弱引用的目的是为了防止内存泄露**,如果是强引用那么除非线程结束,否则无法终止,可能会有内存泄漏的风险。 +- 但是这样还是会存在内存泄露的问题,假如 key 和 ThreadLocal 对象被回收之后,entry 中就存在 key 为 null ,但是 value 有值的 entry 对象,但是永远没办法被访问到,同样除非线程结束运行。**解决方法就是调用 remove 方法删除 entry 对象**。 + +## 34.一个对象的内存布局是怎么样的? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/baguwen/basic-34-16.png) + +- **1.对象头**: + 对象头又分为 **MarkWord** 和 **Class Pointer** 两部分。 + - **MarkWord**:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位,gc记录信息等等。 + - **ClassPointer**:用来指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。在 32 位系统占 4 字节,在 64 位系统中占 8 字节。 +- **2.Length**:只在数组对象中存在,用来记录数组的长度,占用 4 字节 +- **3.Instance data**: + 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的) +- **4.Padding**:Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,**保证对象是 8 字节的整数倍**。 + +--- + +>作者:moon聊技术,转载链接:[https://mp.weixin.qq.com/s/aTWtqPyMQ-6P_c8iuMVrkg](https://mp.weixin.qq.com/s/aTWtqPyMQ-6P_c8iuMVrkg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/baguwen/java-basic.md b/docs/baguwen/java-basic.md new file mode 100644 index 0000000000..0407bbc3e7 --- /dev/null +++ b/docs/baguwen/java-basic.md @@ -0,0 +1,403 @@ +--- +category: + - 求职面试 +tag: + - 背诵版八股文 +--- + +# Java 基础八股文(背诵版)必看:+1: + +### Java 语言具有哪些特点? + +- Java 为纯面向对象的语言。它能够直接反应现实生活中的对象。 +- 具有平台无关性。Java 利用 Java 虚拟机运行字节码,无论是在 Windows、Linux 还是 MacOS 等其它平台对 Java 程序进行编译,编译后的程序可在其它平台运行。 +- Java 为解释型语言,编译器把 Java 代码编译成平台无关的中间代码,然后在 JVM 上解释运行,具有很好的可移植性。 +- Java 提供了很多内置类库。如对多线程支持,对网络通信支持,最重要的一点是提供了垃圾回收器。 +- Java 具有较好的安全性和健壮性。Java 提供了异常处理和垃圾回收机制,去除了 C++中难以理解的指针特性。 + +### JDK 与 JRE 有什么区别? + +- JDK:Java 开发工具包(Java Development Kit),提供了 Java 的开发环境和运行环境。 +- JRE:Java 运行环境(Java Runtime Environment),提供了 Java 运行所需的环境。 +- JDK 包含了 JRE。如果只运行 Java 程序,安装 JRE 即可。要编写 Java 程序需安装 JDK. + +### 简述 Java 基本数据类型 + +- byte: 占用 1 个字节,取值范围-128 ~ 127 +- short: 占用 2 个字节,取值范围-2^15^ ~ 2^15^-1 +- int:占用 4 个字节,取值范围-2^31^ ~ 2^31^-1 +- long:占用 8 个字节 +- float:占用 4 个字节 +- double:占用 8 个字节 +- char: 占用 2 个字节 +- boolean:占用大小根据实现虚拟机不同有所差异 + + + +### 简述自动装箱拆箱 + +对于 Java 基本数据类型,均对应一个包装类。 + +装箱就是自动将基本数据类型转换为包装器类型,如 int->Integer + +拆箱就是自动将包装器类型转换为基本数据类型,如 Integer->int + + + + + +### 简述 Java 访问修饰符 + +- default: 默认访问修饰符,在同一包内可见 +- private: 在同一类内可见,不能修饰类 +- protected : 对同一包内的类和所有子类可见,不能修饰类 +- public: 对所有类可见 + +### 构造方法、成员变量初始化以及静态成员变量三者的初始化顺序? + +先后顺序:静态成员变量、成员变量、构造方法。 + +详细的先后顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。 + +### Java 代码块执行顺序 + +- 父类静态代码块(只执行一次) +- 子类静态代码块(只执行一次) +- 父类构造代码块 +- 父类构造函数 +- 子类构造代码块 +- 子类构造函数 +- 普通代码块 + + + +### 面向对象的三大特性? + +继承:对象的一个新类可以从现有的类中派生,派生类可以从它的基类那继承方法和实例变量,且派生类可以修改或新增新的方法使之更适合特殊的需求。 + +封装:将客观事物抽象成类,每个类可以把自身数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏。 + +多态:允许不同类的对象对同一消息作出响应。不同对象调用相同方法即使参数也相同,最终表现行为是不一样的。 + +### 为什么 Java 语言不支持多重继承? + +为了程序的结构能够更加清晰从而便于维护。假设 Java 语言支持多重继承,类 C 继承自类 A 和类 B,如果类 A 和 B 都有自定义的成员方法 `f()`,那么当代码中调用类 C 的 `f()` 会产生二义性。 + +Java 语言通过实现多个接口间接支持多重继承,接口由于只包含方法定义,不能有方法的实现,类 C 继承接口 A 与接口 B 时即使它们都有方法`f()`,也不能直接调用方法,需实现具体的`f()`方法才能调用,不会产生二义性。 + +多重继承会使类型转换、构造方法的调用顺序变得复杂,会影响到性能。 + +### 简述 Java 的多态 + +Java 多态可以分为编译时多态和运行时多态。 + +编译时多态主要指方法的重载,即通过参数列表的不同来区分不同的方法。 + +运行时多态主要指继承父类和实现接口时,可使用父类引用指向子类对象。 + +运行时多态的实现:主要依靠方法表,方法表中最先存放的是 Object 类的方法,接下来是该类的父类的方法,最后是该类本身的方法。如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法。因此可以实现运行时多态。 + +### Java 提供的多态机制? + +Java 提供了两种用于多态的机制,分别是重载与覆盖。 + +重载:重载是指同一个类中有多个同名的方法,但这些方法有不同的参数,在编译期间就可以确定调用哪个方法。 + +覆盖:覆盖是指派生类重写基类的方法,使用基类指向其子类的实例对象,或接口的引用变量指向其实现类的实例对象,在程序调用的运行期根据引用变量所指的具体实例对象调用正在运行的那个对象的方法,即需要到运行期才能确定调用哪个方法。 + +### 重载与覆盖的区别? + +- 覆盖是父类与子类之间的关系,是垂直关系;重载是同一类中方法之间的关系,是水平关系。 +- 覆盖只能由一个方法或一对方法产生关系;重载是多个方法之间的关系。 +- 覆盖要求参数列表相同;重载要求参数列表不同。 +- 覆盖中,调用方法体是根据对象的类型来决定的,而重载是根据调用时实参表与形参表来对应选择方法体。 +- 重载方法可以改变返回值的类型,覆盖方法不能改变返回值的类型。 + +### 接口和抽象类的相同点和不同点? + +相同点: + +- 都不能被实例化。 +- 接口的实现类或抽象类的子类需实现接口或抽象类中相应的方法才能被实例化。 + +不同点: + +- 接口只能有方法定义,不能有方法的实现,而抽象类可以有方法的定义与实现。 + +- 实现接口的关键字为 implements,继承抽象类的关键字为 extends。一个类可以实现多个接口,只能继承一个抽象类。 + +- 当子类和父类之间存在逻辑上的层次结构,推荐使用抽象类,有利于功能的累积。当功能不需要,希望支持差别较大的两个或更多对象间的特定交互行为,推荐使用接口。使用接口能降低软件系统的耦合度,便于日后维护或添加删除方法。 + +### 简述抽象类与接口的区别 + +抽象类:体现的是 is-a 的关系,如对于 man is a person,就可以将 person 定义为抽象类。 + +接口:体现的是 can 的关系。是作为模板实现的。如设置接口 fly,plane 类和 bird 类均可实现该接口。 + +一个类只能继承一个抽象类,但可以实现多个接口。 + +### 简述内部类及其作用 + +- 成员内部类:作为成员对象的内部类。可以访问 private 及以上外部类的属性和方法。外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法。外部类也可访问 private 修饰的内部类属性。 +- 局部内部类:存在于方法中的内部类。访问权限类似局部变量,只能访问外部类的 final 变量。 +- 匿名内部类:只能使用一次,没有类名,只能访问外部类的 final 变量。 +- 静态内部类:类似类的静态成员变量。 + + + + +### Java 语言中关键字 static 的作用是什么? + static 的主要作用有两个: + +- 为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间。 +- 使得某个方法或属性与类而不是对象关联在一起,即在不创建对象的情况下可通过类直接调用方法或使用类的属性。 + +具体而言 static 又可分为 4 种使用方式: + +- 修饰成员变量。用 static 关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用“类.静态变量”和“对象.静态变量”的方法使用。 +- 修饰成员方法。static 修饰的方法无需创建对象就可以被调用。static 方法中不能使用 this 和 super 关键字,不能调用非 static 方法,只能访问所属类的静态成员变量和静态成员方法。 +- 修饰代码块。JVM 在加载类的时候会执行 static 代码块。static 代码块常用于初始化静态变量。static 代码块只会被执行一次。 +- 修饰内部类。static 内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。 + + + + + +### 为什么要把 String 设计为不可变? + +- 节省空间:字符串常量存储在 JVM 的字符串池中可以被用户共享。 +- 提高效率:String 可以被不同线程共享,是线程安全的。在涉及多线程操作中不需要同步操作。 +- 安全:String 常被用于用户名、密码、文件名等使用,由于其不可变,可避免黑客行为对其恶意修改。 + +### 简述 String/StringBuffer 与 StringBuilder + +String 类采用利用 final 修饰的字符数组进行字符串保存,因此不可变。如果对 String 类型对象修改,需要新建对象,将老字符和新增加的字符一并存进去。 + +StringBuilder,采用无 final 修饰的字符数组进行保存,因此可变。但线程不安全。 + +StringBuffer,采用无 final 修饰的字符数组进行保存,可理解为实现线程安全的 StringBuilder。 + +### 判等运算符==与 equals 的区别? + +== 比较的是引用,equals 比较的是内容。 + +如果变量是基础数据类型,== 用于比较其对应值是否相等。如果变量指向的是对象,== 用于比较两个对象是否指向同一块存储空间。 + +equals 是 Object 类提供的方法之一,每个 Java 类都继承自 Object 类,所以每个对象都具有 equals 这个方法。Object 类中定义的 equals 方法内部是直接调用 == 比较对象的。但通过覆盖的方法可以让它不是比较引用而是比较数据内容。 + +### 简述 Object 类常用方法 + +- hashCode:通过对象计算出的散列码。用于 map 型或 equals 方法。需要保证同一个对象多次调用该方法,总返回相同的整型值。 +- equals:判断两个对象是否一致。需保证 equals 方法相同对应的对象 hashCode 也相同。 +- toString: 用字符串表示该对象 +- clone:深拷贝一个对象 + +### Java 中一维数组和二维数组的声明方式? + +一维数组的声明方式: + +```java +type arrayName[] +type[] arrayName +``` + +二维数组的声明方式: + +```java +type arrayName[][] +type[][] arrayName +type[] arrayName[] +``` + +其中 type 为基本数据类型或类,arrayName 为数组名字 + +### 简述 Java 异常的分类 + +Java 异常分为 Error(程序无法处理的错误),和 Exception(程序本身可以处理的异常)。这两个类均继承 Throwable。 + +Error 常见的有 StackOverFlowError、OutOfMemoryError 等等。 + +Exception 可分为运行时异常和非运行时异常。对于运行时异常,可以利用 try catch 的方式进行处理,也可以不处理。对于非运行时异常,必须处理,不处理的话程序无法通过编译。 + +### 简述 throw 与 throws 的区别 + +throw 一般是用在方法体的内部,由开发者定义当程序语句出现问题后主动抛出一个异常。 + +throws 一般用于方法声明上,代表该方法可能会抛出的异常列表。 + +### 出现在 Java 程序中的 finally 代码块是否一定会执行? + +当遇到下面情况不会执行。 + +- 当程序在进入 try 语句块之前就出现异常时会直接结束。 +- 当程序在 try 块中强制退出时,如使用 System.exit(0),也不会执行 finally 块中的代码。 + +其它情况下,在 try/catch/finally 语句执行的时候,try 块先执行,当有异常发生,catch 和 finally 进行处理后程序就结束了,当没有异常发生,在执行完 finally 中的代码后,后面代码会继续执行。值得注意的是,当 try/catch 语句块中有 return 时,finally 语句块中的代码会在 return 之前执行。如果 try/catch/finally 块中都有 return 语句,finally 块中的 return 语句会覆盖 try/catch 模块中的 return 语句。 + +### final、finally 和 finalize 的区别是什么? + +- final 用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承。 +- finally 作为异常处理的一部分,只能在 try/catch 语句中使用,finally 附带一个语句块用来表示这个语句最终一定被执行,经常被用在需要释放资源的情况下。 +- finalize 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的 finalize()方法。当垃圾回收器准备好释放对象占用空间时,首先会调用 finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存。 + +### 简述泛型 + +泛型,即“参数化类型”,解决不确定对象具体类型的问题。在编译阶段有效。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型在类中称为泛型类、接口中称为泛型接口和方法中称为泛型方法。 + +### 简述泛型擦除 + +Java 编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程被称为泛型擦除。 + +### 简述注解 + +Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。 + +其可以用于提供信息给编译器,在编译阶段时给软件提供信息进行相关的处理,在运行时处理写相应代码,做对应操作。 + +### 简述元注解 + +元注解可以理解为注解的注解,即在注解中使用,实现想要的功能。其具体分为: + +- @Retention: 表示注解存在阶段是保留在源码,还是在字节码(类加载)或者运行期(JVM 中运行)。 +- @Target:表示注解作用的范围。 +- @Documented:将注解中的元素包含到 Javadoc 中去。 +- @Inherited:一个被@Inherited 注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解。 +- @Repeatable:被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。 + + + + +### 简述 Java 中 Class 对象 + +java 中对象可以分为实例对象和 Class 对象,每一个类都有一个 Class 对象,其包含了与该类有关的信息。 + +获取 Class 对象的方法: + +```java +Class.forName(“类的全限定名”) +实例对象.getClass() +类名.class +``` + +### Java 反射机制是什么? + +Java 反射机制是指在程序的运行过程中可以构造任意一个类的对象、获取任意一个类的成员变量和成员方法、获取任意一个对象所属的类信息、调用任意一个对象的属性和方法。反射机制使得 Java 具有动态获取程序信息和动态调用对象方法的能力。可以通过以下类调用反射 API。 + +- Class 类:可获得类属性方法 +- Field 类:获得类的成员变量 +- Method 类:获取类的方法信息 +- Construct 类:获取类的构造方法等信息 + + + + + + + + + + +### 序列化是什么? + +序列化是一种将对象转换成字节序列的过程,用于解决在对对象流进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要的时候把该流读取出来重新构造成一个相同的对象。 + + +### 简述 Java 序列化与反序列化的实现 + +序列化:将 java 对象转化为字节序列,由此可以通过网络对象进行传输。 + +反序列化:将字节序列转化为 java 对象。 + +具体实现:实现 Serializable 接口,或实现 Externalizable 接口中的 writeExternal()与 readExternal()方法。 + +### 简述 Java 的 List + +List 是一个有序队列,在 Java 中有两种实现方式: + +ArrayList 使用数组实现,是容量可变的非线程安全列表,随机访问快,集合扩容时会创建更大的数组,把原有数组复制到新数组。 + +LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。 + +### Java 中线程安全的基本数据结构有哪些 + +- HashTable: 哈希表的线程安全版,效率低 +- ConcurrentHashMap:哈希表的线程安全版,效率高,用于替代 HashTable +- Vector:线程安全版 Arraylist +- Stack:线程安全版栈 +- BlockingQueue 及其子类:线程安全版队列 + +### 简述 Java 的 Set + +Set 即集合,该数据结构不允许元素重复且无序。Java 对 Set 有三种实现方式: + +HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,Value 系统自定义一个名为 PRESENT 的 Object 类型常量。判断元素是否相同时,先比较 hashCode,相同后再利用 equals 比较,查询 O(1) + +LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。 + +TreeSet 通过 TreeMap 实现的,底层数据结构是红黑树,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。查询 O(logn) + +### 简述 Java 的 HashMap + +JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树。主要成员变量包括存储数据的 table 数组、元素数量 size、加载因子 loadFactor。HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样,就会发生哈希冲突,被放到同一个链表上。 + +table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。在 JDK8 后链表超过 8 会转化为红黑树。 + +若当前数据/总数据容量>负载因子,Hashmap 将执行扩容操作。默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。 + +### 为何 HashMap 线程不安全 + +在 JDK1.7 中,HashMap 采用头插法插入元素,因此并发情况下会导致环形链表,产生死循环。 + +虽然 JDK1.8 采用了尾插法解决了这个问题,但是并发下的 put 操作也会使前一个 key 被后一个 key 覆盖。 + +由于 HashMap 有扩容机制存在,也存在 A 线程进行扩容后,B 线程执行 get 方法出现失误的情况。 + +### 简述 Java 的 TreeMap + +TreeMap 是底层利用红黑树实现的 Map 结构,底层实现是一棵平衡的排序二叉树,由于红黑树的插入、删除、遍历时间复杂度都为 O(logN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树可以按照键的值的大小有序输出。 + + + +### ArrayList、Vector 和 LinkedList 有什么共同点与区别? + +- ArrayList、Vector 和 LinkedList 都是可伸缩的数组,即可以动态改变长度的数组。 +- ArrayList 和 Vector 都是基于存储元素的 Object[] array 来实现的,它们会在内存中开辟一块连续的空间来存储,支持下标、索引访问。但在涉及插入元素时可能需要移动容器中的元素,插入效率较低。当存储元素超过容器的初始化容量大小,ArrayList 与 Vector 均会进行扩容。 +- Vector 是线程安全的,其大部分方法是直接或间接同步的。ArrayList 不是线程安全的,其方法不具有同步性质。LinkedList 也不是线程安全的。 +- LinkedList 采用双向列表实现,对数据索引需要从头开始遍历,因此随机访问效率较低,但在插入元素的时候不需要对数据进行移动,插入效率较高。 + +### HashMap 和 Hashtable 有什么区别? + +- HashMap 是 Hashtable 的轻量级实现,HashMap 允许 key 和 value 为 null,但最多允许一条记录的 key 为 null.而 HashTable 不允许。 +- HashTable 中的方法是线程安全的,而 HashMap 不是。在多线程访问 HashMap 需要提供额外的同步机制。 +- Hashtable 使用 Enumeration 进行遍历,HashMap 使用 Iterator 进行遍历。 + +### 如何决定使用 HashMap 还是 TreeMap? + +如果对 Map 进行插入、删除或定位一个元素的操作更频繁,HashMap 是更好的选择。如果需要对 key 集合进行有序的遍历,TreeMap 是更好的选择。 + + + +### HashSet 中,equals 与 hashCode 之间的关系? + +equals 和 hashCode 这两个方法都是从 object 类中继承过来的,equals 主要用于判断对象的内存地址引用是否是同一个地址;hashCode 根据定义的哈希规则将对象的内存地址转换为一个哈希码。HashSet 中存储的元素是不能重复的,主要通过 hashCode 与 equals 两个方法来判断存储的对象是否相同: + +- 如果两个对象的 hashCode 值不同,说明两个对象不相同。 +- 如果两个对象的 hashCode 值相同,接着会调用对象的 equals 方法,如果 equlas 方法的返回结果为 true,那么说明两个对象相同,否则不相同。 + +### fail-fast 和 fail-safe 迭代器的区别是什么? + +- fail-fast 直接在容器上进行,在遍历过程中,一旦发现容器中的数据被修改,就会立刻抛出 ConcurrentModificationException 异常从而导致遍历失败。常见的使用 fail-fast 方式的容器有 HashMap 和 ArrayList 等。 +- fail-safe 这种遍历基于容器的一个克隆。因此对容器中的内容修改不影响遍历。常见的使用 fail-safe 方式遍历的容器有 ConcurrentHashMap 和 CopyOnWriteArrayList。 + +### Collection 和 Collections 有什么区别? + +- Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。 +- Collections 是一个包装类,包含了很多静态方法、不能被实例化,而是作为工具类使用,比如提供的排序方法:Collections.sort(list);提供的反转方法:Collections.reverse(list)。 + +--- + +投稿作者:后端技术小牛说 +转载链接:[https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w](https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/baguwen/java-thread.md b/docs/baguwen/java-thread.md new file mode 100644 index 0000000000..b8ab81a267 --- /dev/null +++ b/docs/baguwen/java-thread.md @@ -0,0 +1,305 @@ +--- +category: + - 求职面试 +tag: + - 背诵版八股文 +--- + +# Java 并发编程八股文(背诵版)必看:+1: + +### 简述Java内存模型(JMM) +Java内存模型定义了程序中各种变量的访问规则: + +- 所有变量都存储在主存,每个线程都有自己的工作内存。 +- 工作内存中保存了被该线程使用的变量的主存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。 +- 操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。 + +### 简述as-if-serial +编译器会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果都必须和用户原始程序输出的预定结果保持一致。 + +### 简述happens-before八大规则 +- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作; +- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作; +- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作; +- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C; +- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作; +- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生; +- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行; +- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始; + +### as-if-serial 和 happens-before 的区别 +as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变。 + +### 简述原子性操作 +一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。 + +### 简述线程的可见性 +可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile、synchronized、final 关键字都能保证可见性。 + +### 简述有序性 +虽然多线程存在并发和指令优化等操作,但在本线程内观察该线程的所有执行操作是有序的。 + +### 简述Java中volatile关键字作用 +- 保证变量对所有线程的可见性。当一个线程修改了变量值,新值对于其他线程来说是立即可以得知的。 +- 禁止指令重排。使用 volatile 变量进行写操作,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器进行重排序。 + +### Java线程的实现方式 +- 实现Runnable接口 +- 继承Thread类 +- 实现Callable接口 +### 简述Java线程的状态 +线程状态有 NEW、RUNNABLE、BLOCK、WAITING、TIMED_WAITING、THERMINATED + +- NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。 +- RUNNABLE:运行状态。表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。 +- BLOCKED:阻塞状态。线程等待获取锁,锁还没获得。 +- WAITING:等待状态。线程内run方法执行完Object.wait()/Thread.join()进入该状态。 +- TIMED_WAITING:限期等待。在一定时间之后跳出状态。调用Thread.sleep(long) Object.wait(long) Thread.join(long)进入状态。其中这些参数代表等待的时间。 +- TERMINATED:结束状态。线程调用完run方法进入该状态。 + +### 简述线程通信的方式 +- volatile 关键词修饰变量,保证所有线程对变量访问的可见性。 +- synchronized关键词。确保多个线程在同一时刻只能有一个处于方法或同步块中。 +- wait/notify方法 +- IO通信 +### 简述线程池 +没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完当前任务后复用已创建的线程,可以降低开销、控制最大并发数。 + +线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。 + +将任务派发给线程池时,会出现以下几种情况 + +- 核心线程池未满,创建一个新的线程执行任务。 +- 如果核心线程池已满,工作队列未满,将线程存储在工作队列。 +- 如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务。 +- 如果超过大小线程数,按照拒绝策略来处理任务。 + +线程池参数: + +- corePoolSize:常驻核心线程数。超过该值后如果线程空闲会被销毁。 +- maximumPoolSize:线程池能够容纳同时执行的线程最大数。 +- keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。 +- workQueue:工作队列。 +- threadFactory:线程工厂,用来生产一组相同任务的线程。 +- handler:拒绝策略。 + +拒绝策略有以下几种: + +- AbortPolicy:丢弃任务并抛出异常 +- CallerRunsPolicy:重新尝试提交该任务 +- DiscardOldestPolicy 抛弃队列里等待最久的任务并把当前任务加入队列 +- DiscardPolicy 表示直接抛弃当前任务但不抛出异常。 + + +### 简述Executor框架 +Executor框架目的是将任务提交和任务如何运行分离开来的机制。用户不再需要从代码层考虑设计任务的提交运行,只需要调用Executor框架实现类的Execute方法就可以提交任务。 + +### 简述Executor的继承关系 +- Executor:一个接口,其定义了一个接收Runnable对象的方法executor,该方法接收一个Runable实例执行这个任务。 +- ExecutorService:Executor的子类接口,其定义了一个接收Callable对象的方法,返回 Future 对象,同时提供execute方法。 +- ScheduledExecutorService:ExecutorService的子类接口,支持定期执行任务。 +- AbstractExecutorService:抽象类,提供 ExecutorService 执行方法的默认实现。 +- Executors:实现ExecutorService接口的静态工厂类,提供了一系列工厂方法用于创建线程池。 +- ThreadPoolExecutor:继承AbstractExecutorService,用于创建线程池。 +- ForkJoinPool: 继承AbstractExecutorService,Fork 将大任务分叉为多个小任务,然后让小任务执行,Join 是获得小任务的结果,类似于map reduce。 +- ThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,用于创建带定时任务的线程池。 +### 简述线程池的状态 +- Running:能接受新提交的任务,也可以处理阻塞队列的任务。 +- Shutdown:不再接受新提交的任务,但可以处理存量任务,线程池处于running时调用shutdown方法,会进入该状态。 +- Stop:不接受新任务,不处理存量任务,调用shutdownnow进入该状态。 +- Tidying:所有任务已经终止了,worker_count(有效线程数)为0。 +- Terminated:线程池彻底终止。在tidying模式下调用terminated方法会进入该状态。 + +### 简述线程池类型 +- newCachedThreadPool 可缓存线程池,可设置最小线程数和最大线程数,线程空闲1分钟后自动销毁。 +- newFixedThreadPool 指定工作线程数量线程池。 +- newSingleThreadExecutor 单线程Executor。 +- newScheduleThreadPool 支持定时任务的指定工作线程数量线程池。 +- newSingleThreadScheduledExecutor 支持定时任务的单线程Executor。 + +### 简述阻塞队列 +阻塞队列是生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有: + +- ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。 +- LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。 +- PriorityBlockingQueue:阻塞优先队列。 +- DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素 +- SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作 +- LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者。 +- LinkedBlockingDeque:双向阻塞队列。 +### 谈一谈ThreadLocal +ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。 + +- set 给ThreadLocalMap设置值。 +- get 获取ThreadLocalMap。 +- remove 删除ThreadLocalMap类型的对象。 + +存在的问题:对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,造成一系列问题。 + +比如说内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。 + +### 聊聊你对Java并发包下unsafe类的理解 +对于 Java 语言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来讲是安全(safe)的。 + +Java 有个类叫 Unsafe 类,这个类使 Java 拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。这个类可以说是 Java 并发开发的基础。 + +### Java中的乐观锁与CAS算法 +乐观锁认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。 + +到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。 + +乐观锁一般都采用 Compare And Swap(CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较(Compare)和交换(Swap)。 + +CAS 算法的思路如下: + +- 该算法认为不同线程对变量的操作时产生竞争的情况比较少。 +- 该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。 +- 如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N。 +- 如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。 + +### ABA问题及解决方法简述 +CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为 A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。 + +juc 包提供了一个 AtomicStampedReference,即在原始的版本下加入版本号戳,解决 ABA 问题。 + +### 简述常见的Atomic类 +在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的++或者--方案,使用synchronized关键字和lock固然可以实现,但代价比较大,此时用原子类更加方便。基本数据类型的原子类有: + +- AtomicInteger 原子更新整型 +- AtomicLong 原子更新长整型 +- AtomicBoolean 原子更新布尔类型 + +Atomic数组类型有: + +- AtomicIntegerArray 原子更新整型数组里的元素 +- AtomicLongArray 原子更新长整型数组里的元素 +- AtomicReferenceArray 原子更新引用类型数组里的元素。 + +Atomic引用类型有: + +- AtomicReference 原子更新引用类型 +- AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记 +- AtomicStampedReference 原子更新带有版本号的引用类型 + +FieldUpdater类型: + +- AtomicIntegerFieldUpdater 原子更新整型字段的更新器 +- AtomicLongFieldUpdater 原子更新长整型字段的更新器 +- AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器 +### 简述Atomic类基本实现原理 +以AtomicIntger 为例。 + +方法getAndIncrement,以原子方式将当前的值加1,具体实现为: + +- 在 for 死循环中取得 AtomicInteger 里存储的数值 +- 对 AtomicInteger 当前的值加 1 +- 调用 compareAndSet 方法进行原子更新 +- 先检查当前数值是否等于 expect +- 如果等于则说明当前值没有被其他线程修改,则将值更新为 next, +- 如果不是会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。 +### 简述CountDownLatch +CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。只能一次性使用,不能reset。 + +### 简述CyclicBarrier +CyclicBarrier 主要功能和CountDownLatch类似,也是通过一个计数器,使一个线程等待其他线程各自执行完毕后再执行。但是其可以重复使用(reset)。 + +### 简述Semaphore +Semaphore即信号量。Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,计数器减一,使用 release 方法归还许可,计数器加一。如果此时计数器值为0,线程进入休眠。 + +### 简述Exchanger +Exchanger类可用于两个线程之间交换信息。可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。线程通过exchange 方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法。当两个线程都到达同步点时这两个线程就可以交换数据当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。 + +### 简述ConcurrentHashMap +JDK7采用锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。 + +get 除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到 Segment,最后通过散列算法定位到元素。put 须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。 + +JDK8的改进 + +- 取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败再使用 synchronized 加锁添加元素 +- 引入红黑树结构,当某个槽内的元素个数超过8且 Node数组 容量大于 64 时,链表转为红黑树。 +- 使用了更加优化的方式统计集合内的元素数量。 +### synchronized底层实现原理 +Java 对象底层都会关联一个 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。 + +synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。 + +执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。 + +### synchronized关键词使用方法 +- 直接修饰某个实例方法 +- 直接修饰某个静态方法 +- 修饰代码块 +### 简述Java偏向锁 +JDK 1.6 中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。 + +其申请流程为: + +- 首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断; +- 判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4; +- 判断是否需要重偏向。如果不用的话,直接获得偏向锁; +- 利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。 +### 简述轻量级锁 +轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。 + +其申请流程为: + +- 如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前 Mark Word 的拷贝。 +- 虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针 +- 如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。 +- 如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧 +- 如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行 +- 如果不是则说明锁对象已经被其他线程抢占。 +- 如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为 10,此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。 +### 简述锁优化策略 +即自适应自旋、锁消除、锁粗化、锁升级等策略偏。 + +### 简述Java的自旋锁 +线程获取锁失败后,可以采用这样的策略,可以不放弃 CPU ,不停的重试内重试,这种操作也称为自旋锁。 + +### 简述自适应自旋锁 +自适应自旋锁自旋次数不再人为设定,通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。 + +### 简述锁粗化 +锁粗化的思想就是扩大加锁范围,避免反复的加锁和解锁。 + +### 简述锁消除 +锁消除是一种更为彻底的优化,在编译时,Java编译器对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。 + +### 简述Lock与ReentrantLock +Lock接口是 Java并发包的顶层接口。 + +可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。 + +### 简述AQS + +AQS(AbstractQuenedSynchronizer)抽象的队列式同步器。AQS是将每一条请求共享资源的线程封装成一个锁队列的一个结点(Node),来实现锁的分配。AQS是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。 + +子类通过继承同步器并实现它的抽象方法getState、setState 和 compareAndSetState对同步状态进行更改。 + +AQS获取独占锁/释放独占锁原理: + +获取:(acquire) + +- 调用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部,在队列中自旋。 +- 调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞。 + +释放:(release) + +- 调用 tryRelease 方法释放同步状态 +- 调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。 + +AQS获取共享锁/释放共享锁原理 + +获取锁(acquireShared) + +- 调用 tryAcquireShared 方法尝试获取同步状态,返回值不小于 0 表示能获取同步状态。 +- 释放(releaseShared),并唤醒后续处于等待状态的节点。 + +---- + + +投稿作者:后端技术小牛说 +转载链接:[https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w](https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/baguwen/jvm.md b/docs/baguwen/jvm.md new file mode 100644 index 0000000000..94643ace85 --- /dev/null +++ b/docs/baguwen/jvm.md @@ -0,0 +1,207 @@ +--- +category: + - 求职面试 +tag: + - 背诵版八股文 +--- + +# Java 虚拟机八股文(背诵版)必看:+1: + +### 简述JVM内存模型 +线程私有的运行时数据区: 程序计数器、Java 虚拟机栈、本地方法栈。 + +线程共享的运行时数据区:Java 堆、方法区。 + +### 简述程序计数器 +程序计数器表示当前线程所执行的字节码的行号指示器。 + +程序计数器不会产生StackOverflowError和OutOfMemoryError。 + +### 简述虚拟机栈 +Java 虚拟机栈用来描述 Java 方法执行的内存模型。线程创建时就会分配一个栈空间,线程结束后栈空间被回收。 + +栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和返回地址等信息。 + +虚拟机栈会产生两类异常: + +- StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。 +- OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。 + +### 简述本地方法栈 +本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为本地方法服务。可以将虚拟机栈看作普通的java函数对应的内存模型,本地方法栈看作由native关键词修饰的函数对应的内存模型。 + +本地方法栈会产生两类异常: + +- StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。 +- OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。 + +### 简述JVM中的堆 +堆主要作用是存放对象实例,Java 里几乎所有对象实例都在堆上分配内存,堆也是内存管理中最大的一块。Java的垃圾回收主要就是针对堆这一区域进行。 可通过 -Xms 和 -Xmx 设置堆的最小和最大容量。 + +堆会抛出 OutOfMemoryError异常。 + +### 简述方法区 +方法区用于存储被虚拟机加载的类信息、常量、静态变量等数据。 + +JDK6之前使用永久代实现方法区,容易内存溢出。JDK7 把放在永久代的字符串常量池、静态变量等移出,JDK8 中抛弃永久代,改用在本地内存中实现的元空间来实现方法区,把 JDK 7 中永久代内容移到元空间。 + +方法区会抛出 OutOfMemoryError异常。 + +### 简述运行时常量池 +运行时常量池存放常量池表,用于存放编译器生成的各种字面量与符号引用。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。除此之外,也会存放字符串基本类型。 + +JDK8之前,放在方法区,大小受限于方法区。JDK8将运行时常量池存放堆中。 + +### 简述直接内存 +直接内存也称为堆外内存,就是把内存对象分配在JVM堆外的内存区域。这部分内存不是虚拟机管理,而是由操作系统来管理。 Java通过DriectByteBuffer对其进行操作,避免了在 Java 堆和 Native堆来回复制数据。 + +### 简述Java创建对象的过程 +- 检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。 +- 通过检查通过后虚拟机将为新生对象分配内存。 +- 完成内存分配后虚拟机将成员变量设为零值 +- 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。 +- 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。 +### 简述JVM给对象分配内存的策略 +- 指针碰撞:这种方式在内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另一边,通过指针挪动完成分配。 +- 空闲列表:对于 Java 堆内存不规整的情况,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。 +### Java对象内存分配是如何保证线程安全的 +第一种方法,采用CAS机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。 + +第二种方法,每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配。一般采用这种策略。 + +### 简述对象的内存布局 +对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。 + +1)对象头主要包含两部分数据: MarkWord、类型指针。 + +MarkWord 用于存储哈希码(HashCode)、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID等信息。 + +类型指针即对象指向他的类元数据指针,如果对象是一个 Java 数组,会有一块用于记录数组长度的数据。 + +2)实例数据存储代码中所定义的各种类型的字段信息。 + +3)对齐填充起占位作用。HotSpot 虚拟机要求对象的起始地址必须是8的整数倍,因此需要对齐填充。 + +### 如何判断对象是否是垃圾 +1)引用计数法: + +设置引用计数器,对象被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。会存在对象间循环引用的问题,一般不使用这种方法。 + +2)可达性分析: + +通过 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,如果某个对象没有被搜到,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。 + +### 简述java的引用类型 +- 强引用: 被强引用关联的对象不会被回收。一般采用 new 方法创建强引用。 +- 软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。一般采用 SoftReference 类来创建软引用。 +- 弱引用:垃圾收集器碰到即回收,也就是说它只能存活到下一次垃圾回收发生之前。一般采用 WeakReference 类来创建弱引用。 +- 虚引用: 无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用。 + +### 简述标记清除算法、标记整理算法和标记复制算法 +- 标记清除算法:先标记需清除的对象,之后统一回收。这种方法效率不高,会产生大量不连续的碎片。 +- 标记整理算法:先标记存活对象,然后让所有存活对象向一端移动,之后清理端边界以外的内存 +- 标记复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。 + +### 简述分代收集算法 +根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 + +一般将堆分为新生代和老年代,对这两块采用不同的算法。 + +新生代使用:标记复制算法 + +老年代使用:标记清除或者标记整理算法 + +### 简述Serial垃圾收集器 +Serial垃圾收集器是单线程串行收集器。垃圾回收的时候,必须暂停其他所有线程。新生代使用标记复制算法,老年代使用标记整理算法。简单高效。 + +### 简述ParNew垃圾收集器 +ParNew垃圾收集器可以看作Serial垃圾收集器的多线程版本,新生代使用标记复制算法,老年代使用标记整理算法。 + +### 简述Parallel Scavenge垃圾收集器 +注重吞吐量,即 CPU运行代码时间/CPU耗时总时间(CPU运行代码时间+ 垃圾回收时间)。新生代使用标记复制算法,老年代使用标记整理算法。 + +### 简述CMS垃圾收集器 +CMS垃圾收集器注重最短时间停顿。CMS垃圾收集器为最早提出的并发收集器,垃圾收集线程与用户线程同时工作。采用标记清除算法。该收集器分为初始标记、并发标记、并发预清理、并发清除、并发重置这么几个步骤。 + +- 初始标记:暂停其他线程(stop the world),标记与GC roots直接关联的对象。 +- 并发标记:可达性分析过程(程序不会停顿)。 +- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象,重新标记,暂停虚拟机(stop the world)扫描CMS堆中剩余对象。 +- 并发清除:清理垃圾对象,(程序不会停顿)。 +- 并发重置,重置CMS收集器的数据结构。 + +### 简述G1垃圾收集器 +和Serial、Parallel Scavenge、CMS不同,G1垃圾收集器把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。 + +- 初始标记:标记与GC roots直接关联的对象。 +- 并发标记:可达性分析。 +- 最终标记:对并发标记过程中,用户线程修改的对象再次标记一下。 +- 筛选回收:对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间制定回收计划并回收。 + +### 简述Minor GC +Minor GC指发生在新生代的垃圾收集,因为 Java 对象大多存活时间短,所以 Minor GC 非常频繁,一般回收速度也比较快。 + +### 简述Full GC +Full GC 是清理整个堆空间—包括年轻代和永久代。调用System.gc(),老年代空间不足,空间分配担保失败,永生代空间不足会产生full gc。 + +### 常见内存分配策略 +大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。 + +大对象需要大量连续内存空间,直接进入老年代区分配。 + +如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1,并且每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。 + +如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。 + +MinorGC 前,虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。如果不,JVM会查看HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将Minor GC,否则改成一次 FullGC。 + +### 简述JVM类加载过程 +1)加载: + +- 通过全类名获取类的二进制字节流。 +- 将类的静态存储结构转化为方法区的运行时数据结构。 +- 在内存中生成类的Class对象,作为方法区数据的入口。 + +2)验证:对文件格式,元数据,字节码,符号引用等验证正确性。 + +3)准备:在方法区内为类变量分配内存并设置为0值。 + +4)解析:将符号引用转化为直接引用。 + +5)初始化:执行类构造器clinit方法,真正初始化。 + +### 简述JVM中的类加载器 +- BootstrapClassLoader启动类加载器:加载/lib下的jar包和类。 由C++编写。 +- ExtensionClassLoader扩展类加载器: /lib/ext目录下的jar包和类。由Java编写。 +- AppClassLoader应用类加载器,加载当前classPath下的jar包和类。由Java编写。 + +### 简述双亲委派机制 +一个类加载器收到类加载请求之后,首先判断当前类是否被加载过。已经被加载的类会直接返回,如果没有被加载,首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当父类加载器无法完成时才尝试自己加载。 + +加载类顺序:BootstrapClassLoader->ExtensionClassLoader->AppClassLoader->CustomClassLoader 检查类是否加载顺序: CustomClassLoader->AppClassLoader->ExtensionClassLoader->BootstrapClassLoader + +### 双亲委派机制的优点 +- 避免类的重复加载。相同的类被不同的类加载器加载会产生不同的类,双亲委派保证了Java程序的稳定运行。 +- 保证核心API不被修改。 +- 如何破坏双亲委派机制 +- 重载loadClass()方法,即自定义类加载器。 + +### 如何构建自定义类加载器 +新建自定义类继承自java.lang.ClassLoader,重写findClass、loadClass、defineClass方法 + +### JVM常见调优参数 + +- -Xms 初始堆大小 +- -Xmx 最大堆大小 +- -XX:NewSize 年轻代大小 +- -XX:MaxNewSize 年轻代最大值 +- -XX:PermSize 永生代初始值 +- -XX:MaxPermSize 永生代最大值 +- -XX:NewRatio 新生代与老年代的比例 + +---- + + +投稿作者:后端技术小牛说 +转载链接:[https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w](https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/48-keywords.md b/docs/basic-extra-meal/48-keywords.md new file mode 100644 index 0000000000..430ff4f65d --- /dev/null +++ b/docs/basic-extra-meal/48-keywords.md @@ -0,0 +1,118 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java中常用的48个关键字 + +“二哥,就我之前学过的这些 Java 代码中,有 public、static、void、main 等等,它们应该都是关键字吧?”三妹的脸上泛着甜甜的笑容,我想她在学习 Java 方面已经变得越来越自信了。 + +“是的,三妹。Java 中的关键字可不少呢!你一下子可能记不了那么多,不过,先保留个印象吧,对以后的学习会很有帮助。” + +PS:按照首字母的自然顺序排列。 + +1. **abstract:** 用于声明抽象类,以及抽象方法。 + +2. **boolean:** 用于将变量声明为布尔值类型,只有 true 和 false 两个值。 + +3. **break:** 用于中断循环或 switch 语句。 + +4. **byte:** 用于声明一个可以容纳 8 个比特的变量。 + +5. **case:** 用于在 switch 语句中标记条件的值。 + +6. **catch:** 用于捕获 try 语句中的异常。 + +7. **char:** 用于声明一个可以容纳无符号 16 位比特的 [Unicode 字符](https://mp.weixin.qq.com/s/pNQjlXOivIgO3pbYc0GnpA)的变量。 + +8. **class:** 用于声明一个类。 + +9. **continue:** 用于继续下一个循环,可以在指定条件下跳过其余代码。 + +10. **default:** 用于指定 switch 语句中除去 case 条件之外的默认代码块。 + +11. **do:** 通常和 while 关键字配合使用,do 后紧跟循环体。 + +12. **double:** 用于声明一个可以容纳 64 位浮点数的变量。 + +13. **else:** 用于指示 if 语句中的备用分支。 + +14. **enum:** 用于定义一组固定的常量(枚举)。 + +15. **extends:** 用于指示一个类是从另一个类或接口继承的。 + +16. **final:** 用于指示该变量是不可更改的。 + +17. **finally:** 和 `try-catch` 配合使用,表示无论是否处理异常,总是执行 finally 块中的代码。 + +18. **float:** 用于声明一个可以容纳 32 位浮点数的变量。 + +19. **for:** 用于声明一个 for 循环,如果循环次数是固定的,建议使用 for 循环。 + +20. **if:** 用于指定条件,如果条件为真,则执行对应代码。 + +21. **implements:** 用于实现接口。 + +22. **import:** 用于导入对应的类或者接口。 + +23. **instanceof:** 用于判断对象是否属于某个类型(class)。 + +24. **int:** 用于声明一个可以容纳 32 位带符号的整数变量。 + +25. **interface:** 用于声明接口。 + +26. **long:** 用于声明一个可以容纳 64 位整数的变量。 + +27. **native:** 用于指定一个方法是通过调用本机接口(非 Java)实现的。 + +28. **new:** 用于创建一个新的对象。 + +29. **null:** 如果一个变量是空的(什么引用也没有指向),就可以将它赋值为 null,和空指针异常息息相关。 + +30. **package:** 用于声明类所在的包。 + +31. **private:** 一个访问权限修饰符,表示方法或变量只对当前类可见。 + +32. **protected:** 一个访问权限修饰符,表示方法或变量对同一包内的类和所有子类可见。 + +33. **public:** 一个访问权限修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。`main()` 方法必须声明为 public。 + +34. **return:** 用于在代码执行完成后返回(一个值)。 + +35. **short:** 用于声明一个可以容纳 16 位整数的变量。 + +36. **static:** 表示该变量或方法是静态变量或静态方法。 + +37. **strictfp:** 并不常见,通常用于修饰一个方法,确保方法体内的浮点数运算在每个平台上执行的结果相同。 + +38. **super:** 可用于调用父类的方法或者字段。 + +39. **switch:** 通常用于三个(以上)的条件判断。 + +40. **synchronized:** 用于指定多线程代码中的同步方法、变量或者代码块。 + +41. **this:** 可用于在方法或构造函数中引用当前对象。 + +42. **throw:** 主动抛出异常。 + +43. **throws:** 用于声明异常。 + +44. **transient:** 修饰的字段不会被序列化。 + +45. **try:** 于包裹要捕获异常的代码块。 + +46. **void:** 用于指定方法没有返回值。 + +47. **volatile:** 保证不同线程对它修饰的变量进行操作时的可见性,即一个线程修改了某个变量的值,新值对其他线程来说是立即可见的。 + +48. **while:** 如果循环次数不固定,建议使用 while 循环。 + + +“好了,三妹,关于 Java 中的关键字就先说这 48 个吧,这只是一个大概的介绍,后面还会对一些特殊的关键字单独拎出来详细地讲,比如说重要的 static、final 等。”转动了一下僵硬的脖子后,我对三妹说。 + +“二哥,你辛苦了,足足 48 个啊,容我好好消化一下。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/basic-extra-meal/Overriding.md b/docs/basic-extra-meal/Overriding.md new file mode 100644 index 0000000000..884a79c90d --- /dev/null +++ b/docs/basic-extra-meal/Overriding.md @@ -0,0 +1,321 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java重写(Overriding)时应当遵守的11条规则 + +重写(Overriding)算是 Java 中一个非常重要的概念,理解重写到底是什么对每个 Java 程序员来说都至关重要,这篇文章就来给大家说说重写过程中应当遵守的 12 条规则。 + +### 01、什么是重写? + +重写带来了一种非常重要的能力,可以让子类重新实现从超类那继承过来的方法。在下面这幅图中,Animal 是父类,Dog 是子类,Dog 重新实现了 `move()` 方法用来和父类进行区分,毕竟狗狗跑起来还是比较有特色的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-1.png) + +重写的方法和被重写的方法,不仅方法名相同,参数也相同,只不过,方法体有所不同。 + +### 02、哪些方法可以被重写? + +**规则一:只能重写继承过来的方法**。 + +因为重写是在子类重新实现从父类继承过来的方法时发生的,所以只能重写继承过来的方法,这很好理解。这就意味着,只能重写那些被 public、protected 或者 default 修饰的方法,private 修饰的方法无法被重写。 + +Animal 类有 `move()`、`eat()` 和 `sleep()` 三个方法: + +```java +public class Animal { + public void move() { } + + protected void eat() { } + + void sleep(){ } +} +``` + +Dog 类来重写这三个方法: + +```java +public class Dog extends Animal { + public void move() { } + + protected void eat() { } + + void sleep(){ } +} +``` + + OK,完全没有问题。但如果父类中的方法是 private 的,就行不通了。 + +```java +public class Animal { + private void move() { } +} +``` + +此时,Dog 类中的 `move()` 方法就不再是一个重写方法了,因为父类的 `move()` 方法是 private 的,对子类并不可见。 + +```java +public class Dog extends Animal { + public void move() { } +} +``` + +### 03、哪些方法不能被重写? + +**规则二:final、static 的方法不能被重写**。 + +一个方法是 final 的就意味着它无法被子类继承到,所以就没办法重写。 + +```java +public class Animal { + final void move() { } +} +``` + +由于父类 Animal 中的 `move()` 是 final 的,所以子类在尝试重写该方法的时候就出现编译错误了! + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-2.png) + +同样的,如果一个方法是 static 的,也不允许重写,因为静态方法可用于父类以及子类的所有实例。 + +```java +public class Animal { + final void move() { } +} +``` + +重写的目的在于根据对象的类型不同而表现出多态,而静态方法不需要创建对象就可以使用。没有了对象,重写所需要的“对象的类型”也就没有存在的意义了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-3.png) + +### 04、重写方法的要求 + +**规则三:重写的方法必须有相同的参数列表**。 + +```java +public class Animal { + void eat(String food) { } +} +``` + +Dog 类中的 `eat()` 方法保持了父类方法 `eat()` 的同一个调调,都有一个参数——String 类型的 food。 + +```java +public class Dog extends Animal { + public void eat(String food) { } +} +``` + +一旦子类没有按照这个规则来,比如说增加了一个参数: + +```java +public class Dog extends Animal { + public void eat(String food, int amount) { } +} +``` + +这就不再是重写的范畴了,当然也不是重载的范畴,因为重载考虑的是同一个类。 + +**规则四:重写的方法必须返回相同的类型**。 + +父类没有返回类型: + +```java +public class Animal { + void eat(String food) { } +} +``` + +子类尝试返回 String: + +```java +public class Dog extends Animal { + public String eat(String food) { + return null; + } +} +``` + +于是就编译出错了(返回类型不兼容)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-4.png) + +**规则五:重写的方法不能使用限制等级更严格的权限修饰符**。 + +可以这样来理解: + +- 如果被重写的方法是 default,那么重写的方法可以是 default、protected 或者 public。 +- 如果被重写的方法是 protected,那么重写的方法只能是 protected 或者 public。 +- 如果被重写的方法是 public, 那么重写的方法就只能是 public。 + +举个例子,父类中的方法是 protected: + +```java +public class Animal { + protected void eat() { } +} +``` + +子类中的方法可以是 public: + +```java +public class Dog extends Animal { + public void eat() { } +} +``` + +如果子类中的方法用了更严格的权限修饰符,编译器就报错了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/Overriding-5.png) + +**规则六:重写后的方法不能抛出比父类中更高级别的异常**。 + +举例来说,如果父类中的方法抛出的是 IOException,那么子类中重写的方法不能抛出 Exception,可以是 IOException 的子类或者不抛出任何异常。这条规则只适用于可检查的异常。 + +可检查(checked)异常必须在源代码中显式地进行捕获处理,不检查(unchecked)异常就是所谓的运行时异常,比如说 NullPointerException、ArrayIndexOutOfBoundsException 之类的,不会在编译器强制要求。 + +父类抛出 IOException: + +```java +public class Animal { + protected void eat() throws IOException { } +} +``` + +子类抛出 FileNotFoundException 是可以满足重写的规则的,因为 FileNotFoundException 是 IOException 的子类。 + +```java +public class Dog extends Animal { + public void eat() throws FileNotFoundException { } +} +``` + +如果子类抛出了一个新的异常,并且是一个 checked 异常: + +```java +public class Dog extends Animal { + public void eat() throws FileNotFoundException, InterruptedException { } +} +``` + +那编译器就会提示错误: + +``` +Error:(9, 16) java: com.itwanger.overriding.Dog中的eat()无法覆盖com.itwanger.overriding.Animal中的eat() + 被覆盖的方法未抛出java.lang.InterruptedException +``` + +但如果子类抛出的是一个 unchecked 异常,那就没有冲突: + +```java +public class Dog extends Animal { + public void eat() throws FileNotFoundException, IllegalArgumentException { } +} +``` + +如果子类抛出的是一个更高级别的异常: + +```java +public class Dog extends Animal { + public void eat() throws Exception { } +} +``` + +编译器同样会提示错误,因为 Exception 是 IOException 的父类。 + +``` +Error:(9, 16) java: com.itwanger.overriding.Dog中的eat()无法覆盖com.itwanger.overriding.Animal中的eat() + 被覆盖的方法未抛出java.lang.Exception +``` + +### 05、如何调用被重写的方法? + +**规则七:可以在子类中通过 super 关键字来调用父类中被重写的方法**。 + +子类继承父类的方法而不是重新实现是很常见的一种做法,在这种情况下,可以按照下面的形式调用父类的方法: + +```java +super.overriddenMethodName(); +``` + +来看例子。 + +```java +public class Animal { + protected void eat() { } +} +``` + +子类重写了 `eat()` 方法,然后在子类的 `eat()` 方法中,可以在方法体的第一行通过 `super.eat()` 调用父类的方法,然后再增加属于自己的代码。 + +```java +public class Dog extends Animal { + public void eat() { + super.eat(); + // Dog-eat + } +} +``` + +### 06、重写和构造方法 + +**规则八:构造方法不能被重写**。 + +因为构造方法很特殊,而且子类的构造方法不能和父类的构造方法同名(类名不同),所以构造方法和重写之间没有任何关系。 + +### 07、重写和抽象方法 + +**规则九:如果一个类继承了抽象类,抽象类中的抽象方法必须在子类中被重写**。 + +先来看这样一个接口类: + +```java +public interface Animal { + void move(); +} +``` + +接口中的方法默认都是抽象方法,通过反编译是可以看得到的: + +```java +public interface Animal +{ + public abstract void move(); +} +``` + +如果一个抽象类实现了 Animal 接口,`move()` 方法不是必须被重写的: + +```java +public abstract class AbstractDog implements Animal { + protected abstract void bark(); +} +``` + +但如果一个类继承了抽象类 AbstractDog,那么 Animal 接口中的 `move()` 方法和抽象类 AbstractDog 中的抽象方法 `bark()` 都必须被重写: + +```java +public class BullDog extends AbstractDog { + + public void move() {} + + protected void bark() {} +} +``` + +### 08、重写和 synchronized 方法 + +**规则十:synchronized 关键字对重写规则没有任何影响**。 + +synchronized 关键字用于在多线程环境中获取和释放监听对象,因此它对重写规则没有任何影响,这就意味着 synchronized 方法可以去重写一个非同步方法。 + +### 09、重写和 strictfp 方法 + +**规则十一:strictfp 关键字对重写规则没有任何影响**。 + +如果你想让浮点运算更加精确,而且不会因为硬件平台的不同导致执行的结果不一致的话,可以在方法上添加 strictfp 关键字。因此 strictfp 关键和重写规则无关。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/basic-extra-meal/annotation.md b/docs/basic-extra-meal/annotation.md similarity index 84% rename from docs/src/basic-extra-meal/annotation.md rename to docs/basic-extra-meal/annotation.md index 829a06f6d1..e096ee8cd1 100644 --- a/docs/src/basic-extra-meal/annotation.md +++ b/docs/basic-extra-meal/annotation.md @@ -1,24 +1,19 @@ --- -title: Java注解,请别小看我。 -shortTitle: Java注解 category: - Java核心 tag: - - Java重要知识点 -description: 本文深入探讨了Java注解的概念、分类及其在实际项目中的应用。通过详细的示例和解释,帮助读者更好地理解和掌握Java注解技术,学会如何自定义注解以及在实际开发中灵活运用,提升代码的可读性和可维护性。 -head: - - - meta - - name: keywords - content: Java,注解,annotation,java 注解,java annotation + - Java --- +# 深入理解Java注解 + “二哥,这节讲注解吗?”三妹问。 -“是的。”我说,“注解是 Java 中非常重要的一部分,但经常被忽视也是真的。之所以这么说是因为我们更倾向成为一名注解的使用者而不是创建者。`@Override` 注解用过吧?[方法重写](https://javabetter.cn/basic-extra-meal/override-overload.html)的时候用到过。但你知道怎么自定义一个注解吗?” +“是的。”我说,“注解是 Java 中非常重要的一部分,但经常被忽视也是真的。之所以这么说是因为我们更倾向成为一名注解的使用者而不是创建者。`@Override` 注解用过吧?但你知道怎么自定义一个注解吗?” 三妹毫不犹豫地摇摇头,摆摆手,不好意思地承认自己的确没有自定义过。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/annotation/annotation-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/annotation/annotation-01.png) “好吧,哥来告诉你吧。” @@ -37,7 +32,7 @@ public class AutowiredTest { } ``` -注意到 `@Autowired` 这个注解了吧?它本来是为 Spring(后面会讲)容器注入 Bean 的,现在被我无情地扔在了字段 name 的身上,但这段代码所在的项目中并没有启用 Spring,意味着 `@Autowired` 注解此时只是一个摆设。 +注意到 `@Autowired` 这个注解了吧?它本来是为 Spring 容器注入 Bean 的,现在被我无情地扔在了字段 name 的身上,但这段代码所在的项目中并没有启用 Spring,意味着 `@Autowired` 注解此时只是一个摆设。 “既然只是个摆设,那你这个地方为什么还要用 `@Autowired` 呢?”三妹好奇地问。 @@ -229,12 +224,4 @@ public class JsonFieldTest { “嗯,你好好复习下,我看会《编译原理》。”说完我拿起桌子边上的一本书就走了。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/box.md b/docs/basic-extra-meal/box.md new file mode 100644 index 0000000000..70ff3b278f --- /dev/null +++ b/docs/basic-extra-meal/box.md @@ -0,0 +1,261 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 深入剖析Java中的拆箱和装箱 + +“哥,听说 Java 的每个基本类型都对应了一个包装类型,比如说 int 的包装类型为 Integer,double 的包装类型为 Double,是这样吗?”从三妹这句话当中,能听得出来,她已经提前预习这块内容了。 + +“是的,三妹。基本类型和包装类型的区别主要有以下 4 点,我来带你学习一下。”我回答说。我们家的斜对面刚好是一所小学,所以时不时还能听到朗朗的读书声,让人心情非常愉快。 + +“三妹,你准备好了吗?我们开始吧。” + +“第一,**包装类型可以为 null,而基本类型不可以**。别小看这一点区别,它使得包装类型可以应用于 POJO 中,而基本类型则不行。” + +“POJO 是什么呢?”遇到不会的就问,三妹在这一点上还是非常兢兢业业的。 + +“POJO 的英文全称是 Plain Ordinary Java Object,翻译一下就是,简单无规则的 Java 对象,只有字段以及对应的 setter 和 getter 方法。” + +```java +class Writer { + private Integer age; + private String name; + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +和 POJO 类似的,还有数据传输对象 DTO(Data Transfer Object,泛指用于展示层与服务层之间的数据传输对象)、视图对象 VO(View Object,把某个页面的数据封装起来)、持久化对象 PO(Persistant Object,可以看成是与数据库中的表映射的 Java 对象)。 + +“那为什么 POJO 的字段必须要用包装类型呢?”三妹问。 + +“《阿里巴巴 Java 开发手册》上有详细的说明,你看。”我打开 PDF,并翻到了对应的内容,指着屏幕念道。 + +>数据库的查询结果可能是 null,如果使用基本类型的话,因为要自动拆箱,就会抛出 NullPointerException 的异常。 + +“什么是自动拆箱呢?” + +“自动拆箱指的是,将包装类型转为基本类型,比如说把 Integer 对象转换成 int 值;对应的,把基本类型转为包装类型,则称为自动装箱。” + +“哦。” + +“那接下来,我们来看第二点不同。**包装类型可用于泛型,而基本类型不可以**,否则就会出现编译错误。”一边说着,我一边在 Intellij IDEA 中噼里啪啦地敲了起来。 + +“三妹,你瞧,编译器提示错误了。” + +```java +List list = new ArrayList<>(); // 提示 Syntax error, insert "Dimensions" to complete ReferenceType +List list = new ArrayList<>(); +``` + +“为什么呢?”三妹及时地问道。 + +“因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本类型是个例外。” + +“那,接下来,我们来说第三点,**基本类型比包装类型更高效**。”我喝了一口茶继续说道。 + +“作为局部变量时,基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。”我一边说着,一边打开 `draw.io` 画起了图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/box-01.png) + +很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间,不仅要存储对象,还要存储引用。假如没有基本类型的话,对于数值这类经常使用到的数据来说,每次都要通过 new 一个包装类型就显得非常笨重。 + +“三妹,你想知道程序运行时,数据都存储在什么地方吗?” + +“嗯嗯,哥,你说说呗。” + +“通常来说,有 4 个地方可以用来存储数据。” + +1)寄存器。这是最快的存储区,因为它位于 CPU 内部,用来暂时存放参与运算的数据和运算结果。 + +2)栈。位于 RAM(Random Access Memory,也叫主存,与 CPU 直接交换数据的内部存储器)中,速度仅次于寄存器。但是,在分配内存的时候,存放在栈中的数据大小与生存周期必须在编译时是确定的,缺乏灵活性。基本数据类型的值和对象的引用通常存储在这块区域。 + +3)堆。也位于 RAM 区,可以动态分配内存大小,编译器不必知道要从堆里分配多少存储空间,生存周期也不必事先告诉编译器,Java 的垃圾收集器会自动收走不再使用的数据,因此可以得到更大的灵活性。但是,运行时动态分配内存和销毁对象都需要占用时间,所以效率比栈低一些。new 创建的对象都会存储在这块区域。 + +4)磁盘。如果数据完全存储在程序之外,就可以不受程序的限制,在程序没有运行时也可以存在。像文件、数据库,就是通过持久化的方式,让对象存放在磁盘上。当需要的时候,再反序列化成程序可以识别的对象。 + +“能明白吗?三妹?” + +“这节讲完后,我再好好消化一下。” + +“那好,我们来说第四点,**两个包装类型的值可以相同,但却不相等**。” + +```java +Integer chenmo = new Integer(10); +Integer wanger = new Integer(10); + +System.out.println(chenmo == wanger); // false +System.out.println(chenmo.equals(wanger )); // true +``` + +“两个包装类型在使用“==”进行判断的时候,判断的是其指向的地址是否相等,由于是两个对象,所以地址是不同的。” + +“而 chenmo.equals(wanger) 的输出结果为 true,是因为 equals() 方法内部比较的是两个 int 值是否相等。” + +```java +private final int value; + +public int intValue() { + return value; +} +public boolean equals(Object obj) { + if (obj instanceof Integer) { + return value == ((Integer)obj).intValue(); + } + return false; +} +``` + +虽然 chenmo 和 wanger 的值都是 10,但他们并不相等。换句话说就是:将“==”操作符应用于包装类型比较的时候,其结果很可能会和预期的不符。 + +“三妹,瞧,`((Integer)obj).intValue()` 这段代码就是用来自动拆箱的。下面,我们来详细地说一说自动装箱和自动拆箱。” + +既然有基本类型和包装类型,肯定有些时候要在它们之间进行转换。把基本类型转换成包装类型的过程叫做装箱(boxing)。反之,把包装类型转换成基本类型的过程叫做拆箱(unboxing)。 + +在 Java 1.5 之前,开发人员要手动进行装拆箱,比如说: + +```java +Integer chenmo = new Integer(10); // 手动装箱 +int wanger = chenmo.intValue(); // 手动拆箱 +``` + +Java 1.5 为了减少开发人员的工作,提供了自动装箱与自动拆箱的功能。这下就方便了。 + +```jav +Integer chenmo = 10; // 自动装箱 +int wanger = chenmo; // 自动拆箱 +``` + +来看一下反编译后的代码。 + +```java +Integer chenmo = Integer.valueOf(10); +int wanger = chenmo.intValue(); +``` + +也就是说,自动装箱是通过 `Integer.valueOf()` 完成的;自动拆箱是通过 `Integer.intValue()` 完成的。 + +“嗯,三妹,给你出一道面试题吧。” + +```java +// 1)基本类型和包装类型 +int a = 100; +Integer b = 100; +System.out.println(a == b); + +// 2)两个包装类型 +Integer c = 100; +Integer d = 100; +System.out.println(c == d); + +// 3) +c = 200; +d = 200; +System.out.println(c == d); +``` + +“给你 3 分钟时间,你先思考下,我去抽根华子,等我回来,然后再来分析一下为什么。” + +。。。。。。 + +“嗯,哥,你过来吧,我说一说我的想法。” + +第一段代码,基本类型和包装类型进行 == 比较,这时候 b 会自动拆箱,直接和 a 比较值,所以结果为 true。 + +第二段代码,两个包装类型都被赋值为了 100,这时候会进行自动装箱,按照你之前说的,将“==”操作符应用于包装类型比较的时候,其结果很可能会和预期的不符,我想结果可能为 false。 + +第三段代码,两个包装类型重新被赋值为了 200,这时候仍然会进行自动装箱,我想结果仍然为 false。 + +“嗯嗯,三妹,你分析的很有逻辑,但第二段代码的结果为 true,是不是感到很奇怪?” + +“为什么会这样呀?”三妹急切地问。 + +“你说的没错,自动装箱是通过 Integer.valueOf() 完成的,我们来看看这个方法的源码就明白为什么了。” + +```java +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +是不是看到了一个之前从来没见过的类——IntegerCache? + +“难道说是 Integer 的缓存类?”三妹做出了自己的判断。 + +“是的,来看一下 IntegerCache 的源码吧。” + +```java +private static class IntegerCache { + static final int low = -128; + static final int high; + static final Integer cache[]; + + static { + // high value may be configured by property + int h = 127; + int i = parseInt(integerCacheHighPropValue); + i = Math.max(i, 127); + h = Math.min(i, Integer.MAX_VALUE - (-low) -1); + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + + // range [-128, 127] must be interned (JLS7 5.1.7) + assert IntegerCache.high >= 127; + } +} +``` + +大致瞟一下这段代码你就全明白了。-128 到 127 之间的数会从 IntegerCache 中取,然后比较,所以第二段代码(100 在这个范围之内)的结果是 true,而第三段代码(200 不在这个范围之内,所以 new 出来了两个 Integer 对象)的结果是 false。 + +“三妹,看完上面的分析之后,我希望你记住一点:**当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象**。” + +“自动装拆箱是一个很好的功能,大大节省了我们开发人员的精力,但也会引发一些麻烦,比如下面这段代码,性能就很差。” + +```java +long t1 = System.currentTimeMillis(); +Long sum = 0L; +for (int i = 0; i < Integer.MAX_VALUE;i++) { + sum += i; +} +long t2 = System.currentTimeMillis(); +System.out.println(t2-t1); +``` + +“知道为什么吗?三妹。” + +“难道是因为 sum 被声明成了包装类型 Long 而不是基本类型 long。”三妹若有所思。 + +“是滴,由于 sum 是个 Long 型,而 i 为 int 类型,`sum += i` 在执行的时候,会先把 i 强转为 long 型,然后再把 sum 拆箱为 long 型进行相加操作,之后再自动装箱为 Long 型赋值给 sum。” + +“三妹,你可以试一下,把 sum 换成 long 型比较一下它们运行的时间。” + +。。。。。。 + +“哇,sum 为 Long 型的时候,足足运行了 5825 毫秒;sum 为 long 型的时候,只需要 679 毫秒。” + +“好了,三妹,今天的主题就先讲到这吧。我再去来根华子。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/class-object.md b/docs/basic-extra-meal/class-object.md similarity index 79% rename from docs/src/basic-extra-meal/class-object.md rename to docs/basic-extra-meal/class-object.md index b49fcb7627..b48749b326 100644 --- a/docs/src/basic-extra-meal/class-object.md +++ b/docs/basic-extra-meal/class-object.md @@ -1,18 +1,11 @@ --- -title: Java 中,先有Class还是先有Object? -shortTitle: 先有Class还是先有Object? category: - Java核心 tag: - - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java 中,先有Class还是先有Object? -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,class,object + - Java --- - +# 先有Class还是先有Object? Java 对象模型中: @@ -21,11 +14,11 @@ Java 对象模型中: 那到底是先有Class还是先有Object? JVM 是怎么处理这个“鸡·蛋”问题呢? -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/class-object-2f47490c-70b8-41b8-9551-42c2f98eea91.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/class-object-2f47490c-70b8-41b8-9551-42c2f98eea91.png) 针对这个问题,我在知乎上看到了 R 大的一个回答,正好解答了我心中的疑惑,就分享出来给各位小伙伴一个参考和启发~ ->作者:RednaxelaFX,整理:沉默王二,参考链接:[https://www.zhihu.com/question/30301819/answer/47539163](https://www.zhihu.com/question/30301819/answer/47539163) +>作者:RednaxelaFX,整理:沉默王二,参考链接:https://www.zhihu.com/question/30301819/answer/47539163 ----- @@ -91,15 +84,8 @@ http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/ade5be2b1758/src/share/vm/cl 分享的最后,二哥要简单说两句,每次看 R 大的内容,总是感觉膝盖忍不住要跪一下,只能说写过 JVM 的男人就是不一样。喜欢研究 CPP 源码的话小伙伴可以再深入学习下,一定会有所收获。 ----- - - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/comparable-omparator.md b/docs/basic-extra-meal/comparable-omparator.md new file mode 100644 index 0000000000..a122e6f100 --- /dev/null +++ b/docs/basic-extra-meal/comparable-omparator.md @@ -0,0 +1,174 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 详解Java中Comparable和Comparator的区别 + +那天,小二去马蜂窝面试,面试官老王一上来就甩给了他一道面试题:请问Comparable和Comparator有什么区别?小二差点笑出声,因为三年前,也就是 2021 年,他在《Java 程序员进阶之路》专栏上看到过这题😆。 + +Comparable 和 Comparator 是 Java 的两个接口,从名字上我们就能够读出来它们俩的相似性:以某种方式来比较两个对象。但它们之间到底有什么区别呢?请随我来,打怪进阶喽! + +### 01、Comparable + +Comparable 接口的定义非常简单,源码如下所示。 + +```java +public interface Comparable { + int compareTo(T t); +} +``` + +如果一个类实现了 Comparable 接口(只需要干一件事,重写 `compareTo()` 方法),就可以按照自己制定的规则将由它创建的对象进行比较。下面给出一个例子。 + +```java +public class Cmower implements Comparable { + private int age; + private String name; + + public Cmower(int age, String name) { + this.age = age; + this.name = name; + } + + @Override + public int compareTo(Cmower o) { + return this.getAge() - o.getAge(); + } + + public static void main(String[] args) { + Cmower wanger = new Cmower(19,"沉默王二"); + Cmower wangsan = new Cmower(16,"沉默王三"); + + if (wanger.compareTo(wangsan) < 0) { + System.out.println(wanger.getName() + "比较年轻有为"); + } else { + System.out.println(wangsan.getName() + "比较年轻有为"); + } + } +} +``` + +在上面的示例中,我创建了一个 Cmower 类,它有两个字段:age 和 name。Cmower 类实现了 Comparable 接口,并重写了 `compareTo()` 方法。 + + +程序输出的结果是“沉默王三比较年轻有为”,因为他比沉默王二小三岁。这个结果有什么凭证吗? + +凭证就在于 `compareTo()` 方法,该方法的返回值可能为负数,零或者正数,代表的意思是该对象按照排序的规则小于、等于或者大于要比较的对象。如果指定对象的类型与此对象不能进行比较,则引发 `ClassCastException` 异常(自从有了泛型,这种情况就少有发生了)。 + +### 02、Comparator + +Comparator 接口的定义相比较于 Comparable 就复杂的多了,不过,核心的方法只有两个,来看一下源码。 + +```java +public interface Comparator { + int compare(T o1, T o2); + boolean equals(Object obj); +} +``` + +第一个方法 `compare(T o1, T o2)` 的返回值可能为负数,零或者正数,代表的意思是第一个对象小于、等于或者大于第二个对象。 + +第二个方法 `equals(Object obj)` 需要传入一个 Object 作为参数,并判断该 Object 是否和 Comparator 保持一致。 + +有时候,我们想让类保持它的原貌,不想主动实现 Comparable 接口,但我们又需要它们之间进行比较,该怎么办呢? + +Comparator 就派上用场了,来看一下示例。 + +1)原封不动的 Cmower 类。 + +```java +public class Cmower { + private int age; + private String name; + + public Cmower(int age, String name) { + this.age = age; + this.name = name; + } +} +``` + +(说好原封不动,getter/setter 吃了啊) + +Cmower 类有两个字段:age 和 name,意味着该类可以按照 age 或者 name 进行排序。 + +2)再来看 Comparator 接口的实现类。 + +```java +public class CmowerComparator implements Comparator { + @Override + public int compare(Cmower o1, Cmower o2) { + return o1.getAge() - o2.getAge(); + } +} +``` + +按照 age 进行比较。当然也可以再实现一个比较器,按照 name 进行自然排序,示例如下。 + +```java +public class CmowerNameComparator implements Comparator { + @Override + public int compare(Cmower o1, Cmower o2) { + if (o1.getName().hashCode() < o2.getName().hashCode()) { + return -1; + } else if (o1.getName().hashCode() == o2.getName().hashCode()) { + return 0; + } + return 1; + } +} +``` + +3)再来看测试类。 + +```java +Cmower wanger = new Cmower(19,"沉默王二"); +Cmower wangsan = new Cmower(16,"沉默王三"); +Cmower wangyi = new Cmower(28,"沉默王一"); + +List list = new ArrayList<>(); +list.add(wanger); +list.add(wangsan); +list.add(wangyi); + +list.sort(new CmowerComparator()); + +for (Cmower c : list) { + System.out.println(c.getName()); +} +``` + +创建了三个对象,age 不同,name 不同,并把它们加入到了 List 当中。然后使用 List 的 `sort()` 方法进行排序,来看一下输出的结果。 + +``` +沉默王三 +沉默王二 +沉默王一 +``` + +这意味着沉默王三的年纪比沉默王二小,排在第一位;沉默王一的年纪比沉默王二大,排在第三位。和我们的预期完全符合。 + +### 03、到底该用哪一个呢? + +通过上面的两个例子可以比较出 Comparable 和 Comparator 两者之间的区别: + +- 一个类实现了 Comparable 接口,意味着该类的对象可以直接进行比较(排序),但比较(排序)的方式只有一种,很单一。 +- 一个类如果想要保持原样,又需要进行不同方式的比较(排序),就可以定制比较器(实现 Comparator 接口)。 +- Comparable 接口在 `java.lang` 包下,而 `Comparator` 接口在 `java.util` 包下,算不上是亲兄弟,但可以称得上是表(堂)兄弟。 + +举个不恰当的例子。我想从洛阳出发去北京看长城,体验一下好汉的感觉,要么坐飞机,要么坐高铁;但如果是孙悟空的话,翻个筋斗就到了。我和孙悟空之间有什么区别呢?孙悟空自己实现了 Comparable 接口(他那年代也没有飞机和高铁,没得选),而我可以借助 Comparator 接口(现代化的交通工具)。 + + + + + +------ + + +好了,关于 Comparable 和 Comparator 我们就先聊这么多。总而言之,如果对象的排序需要基于自然顺序,请选择 `Comparable`,如果需要按照对象的不同属性进行排序,请选择 `Comparator`。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/deep-copy.md b/docs/basic-extra-meal/deep-copy.md similarity index 83% rename from docs/src/basic-extra-meal/deep-copy.md rename to docs/basic-extra-meal/deep-copy.md index f5e662fa53..fb7d19dc55 100644 --- a/docs/src/basic-extra-meal/deep-copy.md +++ b/docs/basic-extra-meal/deep-copy.md @@ -1,31 +1,24 @@ --- -title: 深入理解Java浅拷贝与深拷贝 -shortTitle: 深入理解Java浅拷贝与深拷贝 category: - Java核心 tag: - - Java重要知识点 -description: 本文详细讨论了Java中的浅拷贝和深拷贝概念,解析了它们如何在实际编程中应用。文章通过实例演示了如何实现浅拷贝与深拷贝,以帮助读者更好地理解这两种拷贝方式在Java编程中的作用与应用场景。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,深拷贝,浅拷贝 + - Java --- +# 彻底讲明白的Java浅拷贝与深拷贝 + “哥,听说浅拷贝和深拷贝是 Java 面试中经常会被问到的一个问题,是这样吗?” -“还真的是,而且了解浅拷贝和深拷贝的原理,对 [Java 是值传递还是引用传递](https://javabetter.cn/basic-extra-meal/pass-by-value.html)也会有更深的理解。”我肯定地回答。 +“还真的是,而且了解浅拷贝和深拷贝的原理,对 Java 是值传递还是引用传递也会有更深的理解。”我肯定地回答。 “不管是浅拷贝还是深拷贝,都可以通过调用 Object 类的 `clone()` 方法来完成。”我一边说,一边打开 Intellij IDEA,并找到了 `clone()` 方法的源码。 ```java +@HotSpotIntrinsicCandidate protected native Object clone() throws CloneNotSupportedException; ``` -需要注意的是,`clone()` 方法同时是一个本地(`native`)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。 - ->Java 9 后,该方法会被标注 `@HotSpotIntrinsicCandidate` 注解,被该注解标注的方法,在 HotSpot 虚拟机中会有一套高效的实现。 +其中 `@HotSpotIntrinsicCandidate` 是 Java 9 引入的一个注解,被它标注的方法,在 HotSpot 虚拟机中会有一套高效的实现。需要注意的是,`clone()` 方法同时是一个本地(`native`)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。 “哥,那你就先说浅拷贝吧!” @@ -45,12 +38,21 @@ class Writer implements Cloneable{ ", name='" + name + '\'' + '}'; } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } } ``` -Writer 类有两个字段,分别是 int 类型的 age,和 String 类型的 name。然后重写了 `toString()` 方法,方便打印对象的具体信息。 +Writer 类有两个字段,分别是 int 类型的 age,和 String 类型的 name。然后重写了 `toString()` 方法,方便打印对象的具体信息。并且重写了 `clone()` 方法,方法体里面也很简单,直接调用 Object 类的 `clone()` 方法。 -“为什么要实现 Cloneable 接口呢?”三妹开启了十万个为什么的模式。 +“既然 Writer 类的 `clone()` 方法体里只有一行代码,调用的还是超类 Object 的 `clone()` 方法?为什么还要重写呢?不是多此一举吗?”三妹着急地问。 + +“嗯,是这样的,三妹。Object 类中的 `clone()` 方法是 protected 的,如果 Writer 类不去重写的话,Writer 类的对象是无法调用 `clone()` 方法的,因为 protected 修饰的方法对子类并不可见。” + +“哦哦,那为什么要实现 Cloneable 接口呢?”三妹开启了十万个为什么的模式。 Cloneable 接口是一个标记接口,它肚子里面是空的: @@ -109,7 +111,7 @@ writer2:Writer@b97c004{age=18, name='三妹'} 可以看得出,浅拷贝后,writer1 和 writer2 引用了不同的对象,但值是相同的,说明拷贝成功。之后,修改了 writer2 的 name 字段,直接上图就明白了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/deep-copy-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/deep-copy-01.png) 之前的例子中,Writer 类只有两个字段,没有引用类型字段。那么,我们再来看另外一个例子,为 Writer 类增加一个自定义的引用类型字段 Book,先来看 Book 的定义。 @@ -208,7 +210,7 @@ writer2:Writer@36d4b5c age=18, name='二哥', book=Book@32e6e9c3 bookName='永 与之前例子不同的是,writer2.book 变更后,writer1.book 也发生了改变。这是因为字符串 String 是不可变对象,一个新的值必须在字符串常量池中开辟一段新的内存空间,而自定义对象的内存地址并没有发生改变,只是对应的字段值发生了改变,见下图。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/deep-copy-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/deep-copy-02.png) “哇,哥,果真一图胜千言,我明白了。”三妹似乎对我画的图很感兴趣呢,“那你继续说深拷贝吧!” @@ -309,15 +311,15 @@ writer2:Writer@6d00a15d age=18, name='二哥', book=Book@51efea79 bookName=' 不只是 writer1 和 writer2 是不同的对象,它们中的 book 也是不同的对象。所以,改变了 writer2 中的 book 并不会影响到 writer1。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/deep-copy-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/deep-copy-03.png) 不过,通过 `clone()` 方法实现的深拷贝比较笨重,因为要将所有的引用类型都重写 `clone()` 方法,当嵌套的对象比较多的时候,就废了! “那有没有好的办法呢?”三妹急切的问。 -“当然有了,利用[序列化](https://javabetter.cn/io/serialize.html)。”我胸有成竹的回答,“序列化是将对象写到流中便于传输,而反序列化则是将对象从流中读取出来。” +“当然有了,利用序列化。”我胸有成竹的回答,“序列化是将对象写到流中便于传输,而反序列化则是将对象从流中读取出来。” -“写入流中的对象就是对原始对象的拷贝。需要注意的是,每个要序列化的类都要实现 [Serializable 接口](https://javabetter.cn/io/Serializbale.html),该接口和 Cloneable 接口类似,都是标记型接口。” +“写入流中的对象就是对原始对象的拷贝。需要注意的是,每个要序列化的类都要实现 Serializable 接口,该接口和 Cloneable 接口类似,都是标记型接口。” 来看例子。 @@ -421,12 +423,4 @@ writer2:Writer@544fe44c age=18, name='二哥', book=Book@31610302 bookName=' “嗯嗯。” - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/enum.md b/docs/basic-extra-meal/enum.md new file mode 100644 index 0000000000..7016485f5a --- /dev/null +++ b/docs/basic-extra-meal/enum.md @@ -0,0 +1,292 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java枚举(enum) + +“今天我们来学习枚举吧,三妹!”我说,“同学让你去她家玩了两天,感觉怎么样呀?” + +“心情放松了不少。”三妹说,“可以开始学 Java 了,二哥。” + +“OK。” + +“枚举(enum),是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,继承自 java.lang.Enum。” + +“我们来新建一个枚举 PlayerType。” + +```java +public enum PlayerType { + TENNIS, + FOOTBALL, + BASKETBALL +} +``` + +“二哥,我没看到有继承关系呀!” + +“别着急,看一下反编译后的字节码,你就明白了。” + +```java +public final class PlayerType extends Enum +{ + + public static PlayerType[] values() + { + return (PlayerType[])$VALUES.clone(); + } + + public static PlayerType valueOf(String name) + { + return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name); + } + + private PlayerType(String s, int i) + { + super(s, i); + } + + public static final PlayerType TENNIS; + public static final PlayerType FOOTBALL; + public static final PlayerType BASKETBALL; + private static final PlayerType $VALUES[]; + + static + { + TENNIS = new PlayerType("TENNIS", 0); + FOOTBALL = new PlayerType("FOOTBALL", 1); + BASKETBALL = new PlayerType("BASKETBALL", 2); + $VALUES = (new PlayerType[] { + TENNIS, FOOTBALL, BASKETBALL + }); + } +} +``` + +“看到没?Java 编译器帮我们做了很多隐式的工作,不然手写一个枚举就没那么省心省事了。” + +- 要继承 Enum 类; +- 要写构造方法; +- 要声明静态变量和数组; +- 要用 static 块来初始化静态变量和数组; +- 要提供静态方法,比如说 `values()` 和 `valueOf(String name)`。 + +“确实,作为开发者,我们的代码量减少了,枚举看起来简洁明了。”三妹说。 + +“既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用。”我说。 + +```java +public class Player { + private PlayerType type; + public enum PlayerType { + TENNIS, + FOOTBALL, + BASKETBALL + } + + public boolean isBasketballPlayer() { + return getType() == PlayerType.BASKETBALL; + } + + public PlayerType getType() { + return type; + } + + public void setType(PlayerType type) { + this.type = type; + } +} +``` + +PlayerType 就相当于 Player 的内部类。 + +由于枚举是 final 的,所以可以确保在 Java 虚拟机中仅有一个常量对象,基于这个原因,我们可以使用“==”运算符来比较两个枚举是否相等,参照 `isBasketballPlayer()` 方法。 + +“那为什么不使用 `equals()` 方法判断呢?”三妹问。 + +```java +if(player.getType().equals(Player.PlayerType.BASKETBALL)){}; +``` + +“我来给你解释下。” + +“==”运算符比较的时候,如果两个对象都为 null,并不会发生 `NullPointerException`,而 `equals()` 方法则会。 + +另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 `equals()` 方法则不会。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/enum/enum-01.png) + +“枚举还可用于 switch 语句,和基本数据类型的用法一致。”我说。 + +```java +switch (playerType) { + case TENNIS: + return "网球运动员费德勒"; + case FOOTBALL: + return "足球运动员C罗"; + case BASKETBALL: + return "篮球运动员詹姆斯"; + case UNKNOWN: + throw new IllegalArgumentException("未知"); + default: + throw new IllegalArgumentException( + "运动员类型: " + playerType); + + } +``` + +“如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。”我继续说。 + +```java +public enum PlayerType { + TENNIS("网球"), + FOOTBALL("足球"), + BASKETBALL("篮球"); + + private String name; + + PlayerType(String name) { + this.name = name; + } +} +``` + +“get 了吧,三妹?” + +“嗯,比较好理解。” + +“那接下来,我就来说点不一样的。” + +“来吧,我准备好了。” + +“EnumSet 是一个专门针对枚举类型的 Set 接口(后面会讲)的实现类,它是处理枚举类型数据的一把利器,非常高效。”我说,“从名字上就可以看得出,EnumSet 不仅和 Set 有关系,和枚举也有关系。” + +“因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法。” + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/enum/enum-02.png) + +“来看下面这个例子,我们使用 `noneOf()` 静态工厂方法创建了一个空的 PlayerType 类型的 EnumSet;使用 `allOf()` 静态工厂方法创建了一个包含所有 PlayerType 类型的 EnumSet。” + +```java +public class EnumSetTest { + public enum PlayerType { + TENNIS, + FOOTBALL, + BASKETBALL + } + + public static void main(String[] args) { + EnumSet enumSetNone = EnumSet.noneOf(PlayerType.class); + System.out.println(enumSetNone); + + EnumSet enumSetAll = EnumSet.allOf(PlayerType.class); + System.out.println(enumSetAll); + } +} +``` + +“来看一下输出结果。” + +```java +[] +[TENNIS, FOOTBALL, BASKETBALL] +``` + +有了 EnumSet 后,就可以使用 Set 的一些方法了,见下图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/enum/enum-03.png) + +“除了 EnumSet,还有 EnumMap,是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素。” + +“和 EnumSet 不同,EnumMap 不是一个抽象类,所以创建 EnumMap 时可以使用 new 关键字。” + +```java +EnumMap enumMap = new EnumMap<>(PlayerType.class); +``` + +有了 EnumMap 对象后就可以使用 Map 的一些方法了,见下图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/enum/enum-04.png) + +和 HashMap(后面会讲)的使用方法大致相同,来看下面的例子。 + +```java +EnumMap enumMap = new EnumMap<>(PlayerType.class); +enumMap.put(PlayerType.BASKETBALL,"篮球运动员"); +enumMap.put(PlayerType.FOOTBALL,"足球运动员"); +enumMap.put(PlayerType.TENNIS,"网球运动员"); +System.out.println(enumMap); + +System.out.println(enumMap.get(PlayerType.BASKETBALL)); +System.out.println(enumMap.containsKey(PlayerType.BASKETBALL)); +System.out.println(enumMap.remove(PlayerType.BASKETBALL)); +``` + +“来看一下输出结果。” + +``` +{TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员} +篮球运动员 +true +篮球运动员 +``` + +“除了以上这些,《Effective Java》这本书里还提到了一点,如果要实现单例的话,最好使用枚举的方式。”我说。 + +“等等二哥,单例是什么?”三妹没等我往下说,就连忙问道。 + +“单例(Singleton)用来保证一个类仅有一个对象,并提供一个访问它的全局访问点,在一个进程中。因为这个类只有一个对象,所以就不能再使用 `new` 关键字来创建新的对象了。” + +“Java 标准库有一些类就是单例,比如说 Runtime 这个类。” + +```java +Runtime runtime = Runtime.getRuntime(); +``` + +“Runtime 类可以用来获取 Java 程序运行时的环境。” + +“关于单例,懂了些吧?”我问三妹。 + +“噢噢噢噢。”三妹点了点头。 + +“通常情况下,实现单例并非易事,来看下面这种写法。” + +```java +public class Singleton { + private volatile static Singleton singleton; + private Singleton (){} + public static Singleton getSingleton() { + if (singleton == null) { + synchronized (Singleton.class) { + if (singleton == null) { + singleton = new Singleton(); + } + } + } + return singleton; + } +} +``` + +“要用到 volatile、synchronized 关键字等等,但枚举的出现,让代码量减少到极致。” + +```java +public enum EasySingleton{ + INSTANCE; +} +``` + +“就这?”三妹睁大了眼睛。 + +“对啊,枚举默认实现了 Serializable 接口,因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。”我说。 + +“好了,关于枚举就讲这么多吧,三妹,你把这些代码都手敲一遍吧!” + +“好勒,这就安排。二哥,你去休息吧。” + +“嗯嗯。”讲了这么多,必须跑去抽烟机那里安排一根华子了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/equals-hashcode.md b/docs/basic-extra-meal/equals-hashcode.md new file mode 100644 index 0000000000..ad6f1b236e --- /dev/null +++ b/docs/basic-extra-meal/equals-hashcode.md @@ -0,0 +1,233 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 一次性搞清楚equals和hashCode + +“二哥,我在读《Effective Java》 的时候,第 11 条规约说重写 equals 的时候必须要重写 hashCode 方法,这是为什么呀?”三妹单刀直入地问。 + +“三妹啊,这个问题问得非常好,因为它也是面试中经常考的一个知识点。今天哥就带你来梳理一下。”我说。 + +Java 是一门面向对象的编程语言,所有的类都会默认继承自 Object 类,而 Object 的中文意思就是“对象”。 + +Object 类中有这么两个方法: + +```java +public native int hashCode(); + +public boolean equals(Object obj) { + return (this == obj); +} +``` +1)hashCode 方法 + +这是一个本地方法,用来返回对象的哈希值(一个整数)。在 Java 程序执行期间,对同一个对象多次调用该方法必须返回相同的哈希值。 + +2)equals 方法 + +对于任何非空引用 x 和 y,当且仅当 x 和 y 引用的是同一个对象时,equals 方法才返回 true。 + +“二哥,看起来两个方法之间没有任何关联啊?”三妹质疑道。 + +“单从这两段解释上来看,的确是这样的。”我解释道,“但两个方法的 doc 文档中还有这样两条信息。” + +第一,如果两个对象调用 equals 方法返回的结果为 true,那么两个对象调用 hashCode 方法返回的结果也必然相同——来自 hashCode 方法的 doc 文档。 + +第二,每当重写 equals 方法时,hashCode 方法也需要重写,以便维护上一条规约。 + +“哦,这样讲的话,两个方法确实关联上了,但究竟是为什么呢?”三妹抛出了终极一问。 + +“hashCode 方法的作用是用来获取哈希值,而该哈希值的作用是用来确定对象在哈希表中的索引位置。”我说。 + +哈希表的典型代表就是 HashMap,它存储的是键值对,能根据键快速地检索出对应的值。 + +```java +public V get(Object key) { + HashMap.Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} +``` + +这是 HashMap 的 get 方法,通过键来获取值的方法。它会调用 getNode 方法: + +```java +final HashMap.Node getNode(int hash, Object key) { + HashMap.Node[] tab; HashMap.Node first, e; int n; K k; + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + if ((e = first.next) != null) { + if (first instanceof HashMap.TreeNode) + return ((HashMap.TreeNode)first).getTreeNode(hash, key); + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + +通常情况(没有发生哈希冲突)下,`first = tab[(n - 1) & hash]` 就是键对应的值。**按照时间复杂度来说的话,可表示为 O(1)**。 + +如果发生哈希冲突,也就是 `if ((e = first.next) != null) {}` 子句中,可以看到如果节点不是红黑树的时候,会通过 do-while 循环语句判断键是否 equals 返回对应值的。**按照时间复杂度来说的话,可表示为 O(n)**。 + +HashMap 是通过拉链法来解决哈希冲突的,也就是如果发生哈希冲突,同一个键的坑位会放好多个值,超过 8 个值后改为红黑树,为了提高查询的效率。 + +显然,从时间复杂度上来看的话 O(n) 比 O(1) 的性能要差,这也正是哈希表的价值所在。 + +“O(n) 和 O(1) 是什么呀?”三妹有些不解。 + +“这是时间复杂度的一种表示方法,随后二哥专门给你讲一下。简单说一下 n 和 1 的意思,很显然,n 和 1 都代表的是代码执行的次数,假如数据规模为 n,n 就代表需要执行 n 次,1 就代表只需要执行一次。”我解释道。 + +“三妹,你想一下,如果没有哈希表,但又需要这样一个数据结构,它里面存放的数据是不允许重复的,该怎么办呢?”我问。 + +“要不使用 equals 方法进行逐个比较?”三妹有些不太确定。 + +“这种方法当然是可行的,就像 `if ((e = first.next) != null) {}` 子句中那样,但如果数据量特别特别大,性能就会很差,最好的解决方案还是 HashMap。” + +HashMap 本质上是通过数组实现的,当我们要从 HashMap 中获取某个值时,实际上是要获取数组中某个位置的元素,而位置是通过键来确定的。 + +```java +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} +``` + +这是 HashMap 的 put 方法,会将键值对放入到数组当中。它会调用 putVal 方法: + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + HashMap.Node[] tab; HashMap.Node p; int n, i; + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else { + // 拉链 + } + return null; +} +``` + +通常情况下,`p = tab[i = (n - 1) & hash])` 就是键对应的值。而数组的索引 `(n - 1) & hash` 正是基于 hashCode 方法计算得到的。 + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +“那二哥,你好像还是没有说为什么重写 equals 方法的时候要重写 hashCode 方法呀?”三妹忍不住了。 + +“来看下面这段代码。”我说。 + +```java +public class Test { + public static void main(String[] args) { + Student s1 = new Student(18, "张三"); + Map scores = new HashMap<>(); + scores.put(s1, 98); + + Student s2 = new Student(18, "张三"); + System.out.println(scores.get(s2)); + } +} + class Student { + private int age; + private String name; + + public Student(int age, String name) { + this.age = age; + this.name = name; + } + + @Override + public boolean equals(Object o) { + Student student = (Student) o; + return age == student.age && + Objects.equals(name, student.name); + } + } +``` + +我们重写了 Student 类的 equals 方法,如果两个学生的年纪和姓名相同,我们就认为是同一个学生,虽然很离谱,但我们就是这么草率。 + +在 main 方法中,18 岁的张三考试得了 98 分,很不错的成绩,我们把张三和他的成绩放到 HashMap 中,然后准备取出: + +``` +null +``` + +“二哥,怎么输出了 null,而不是预期当中的 98 呢?”三妹感到很不可思议。 + +“原因就在于重写 equals 方法的时候没有重写 hashCode 方法。”我回答道,“equals 方法虽然认定名字和年纪相同就是同一个学生,但它们本质上是两个对象,hashCode 并不相同。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/equals-hashcode-01.png) + +“那怎么重写 hashCode 方法呢?”三妹问。 + +“可以直接调用 Objects 类的 hash 方法。”我回答。 + +```java + @Override + public int hashCode() { + return Objects.hash(age, name); + } +``` + +Objects 类的 hash 方法可以针对不同数量的参数生成新的哈希值,hash 方法调用的是 Arrays 类的 hashCode 方法,该方法源码如下: + +```java +public static int hashCode(Object a[]) { + if (a == null) + return 0; + + int result = 1; + + for (Object element : a) + result = 31 * result + (element == null ? 0 : element.hashCode()); + + return result; +} +``` + +第一次循环: + +``` +result = 31*1 + Integer(18).hashCode(); +``` + +第二次循环: + +``` +result = (31*1 + Integer(18).hashCode()) * 31 + String("张三").hashCode(); +``` + +针对姓名年纪不同的对象,这样计算后的哈希值很难很难很难重复的;针对姓名年纪相同的对象,哈希值保持一致。 + +再次执行 main 方法,结果如下所示: + +``` +98 +``` + +因为此时 s1 和 s2 对象的哈希值都为 776408。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/equals-hashcode-02.png) + + +“每当重写 equals 方法时,hashCode 方法也需要重写,原因就是为了保证:如果两个对象调用 equals 方法返回的结果为 true,那么两个对象调用 hashCode 方法返回的结果也必然相同。”我点题了。 + +“OK,get 了。”三妹开心地点了点头,看得出来,今天学到了不少。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-extra-meal/fanshe.md b/docs/basic-extra-meal/fanshe.md new file mode 100644 index 0000000000..c28b1ee32c --- /dev/null +++ b/docs/basic-extra-meal/fanshe.md @@ -0,0 +1,323 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 大白话说Java反射:入门、使用、原理 + +“二哥,什么是反射呀?”三妹开门见山地问。 + +“要想知道什么是反射,就需要先来了解什么是‘正射’。”我笑着对三妹说,“一般情况下,我们在使用某个类之前已经确定它到底是个什么类了,拿到手就直接可以使用 `new` 关键字来调用构造方法进行初始化,之后使用这个类的对象来进行操作。” + +```java +Writer writer = new Writer(); +writer.setName("沉默王二"); +``` + +像上面这个例子,就可以理解为“正射”。而反射就意味着一开始我们不知道要初始化的类到底是什么,也就没法直接使用 `new` 关键字创建对象了。 + +我们只知道这个类的一些基本信息,就好像我们看电影的时候,为了抓住一个犯罪嫌疑人,警察就会问一些目击证人,根据这些证人提供的信息,找专家把犯罪嫌疑人的样貌给画出来——这个过程,就可以称之为**反射**。 + +```java +Class clazz = Class.forName("com.itwanger.s39.Writer"); +Method method = clazz.getMethod("setName", String.class); +Constructor constructor = clazz.getConstructor(); +Object object = constructor.newInstance(); +method.invoke(object,"沉默王二"); +``` + +像上面这个例子,就可以理解为“反射”。 + +“反射的写法比正射复杂得多啊!”三妹感慨地说。 + +“是的,反射的成本是要比正射的高得多。”我说,“反射的缺点主要有两个。” + +- **破坏封装**:由于反射允许访问私有字段和私有方法,所以可能会破坏封装而导致安全问题。 +- **性能开销**:由于反射涉及到动态解析,因此无法执行 Java 虚拟机优化,再加上反射的写法的确要复杂得多,所以性能要比“正射”差很多,在一些性能敏感的程序中应该避免使用反射。 + +“那反射有哪些好处呢?”三妹问。 + +反射的主要应用场景有: + +- **开发通用框架**:像 Spring,为了保持通用性,通过配置文件来加载不同的对象,调用不同的方法。 +- **动态代理**:在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式,而动态代理的底层技术就是反射。 +- **注解**:注解本身只是起到一个标记符的作用,它需要利用发射机制,根据标记符去执行特定的行为。 + +“好了,来看一下完整的例子吧。”我对三妹说。 + +Writer 类,有两个字段,然后还有对应的 getter/setter。 + +```java +public class Writer { + private int age; + private String name; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +测试类: + +```java +public class ReflectionDemo1 { + public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + Writer writer = new Writer(); + writer.setName("沉默王二"); + System.out.println(writer.getName()); + + Class clazz = Class.forName("com.itwanger.s39.Writer"); + Constructor constructor = clazz.getConstructor(); + Object object = constructor.newInstance(); + + Method setNameMethod = clazz.getMethod("setName", String.class); + setNameMethod.invoke(object, "沉默王二"); + Method getNameMethod = clazz.getMethod("getName"); + System.out.println(getNameMethod.invoke(object)); + } +} +``` + +来看一下输出结果: + +``` +沉默王二 +沉默王二 +``` + +只不过,反射的过程略显曲折了一些。 + +第一步,获取反射类的 Class 对象: + +```java +Class clazz = Class.forName("com.itwanger.s39.Writer"); +``` + +第二步,通过 Class 对象获取构造方法 Constructor 对象: + +```java +Constructor constructor = clazz.getConstructor(); +``` + +第三步,通过 Constructor 对象初始化反射类对象: + +```java +Object object = constructor.newInstance(); +``` + +第四步,获取要调用的方法的 Method 对象: + +```java +Method setNameMethod = clazz.getMethod("setName", String.class); +Method getNameMethod = clazz.getMethod("getName"); +``` + +第五步,通过 `invoke()` 方法执行: + +```java +setNameMethod.invoke(object, "沉默王二"); +getNameMethod.invoke(object) +``` + +“三妹,你看,经过这五个步骤,基本上就掌握了反射的使用方法。”我说。 + +“好像反射也没什么复杂的啊!”三妹说。 + +我先对三妹点点头,然后说:“是的,掌握反射的基本使用方法确实不难,但要理解整个反射机制还是需要花一点时间去了解一下 Java 虚拟机的类加载机制的。” + +要想使用反射,首先需要获得反射类的 Class 对象,每一个类,不管它最终生成了多少个对象,这些对象只会对应一个 Class 对象,这个 Class 对象是由 Java 虚拟机生成的,由它来获悉整个类的结构信息。 + +也就是说,`java.lang.Class` 是所有反射 API 的入口。 + +而方法的反射调用,最终是由 Method 对象的 `invoke()` 方法完成的,来看一下源码(JDK 8 环境下)。 + +```java +@CallerSensitive +public Object invoke(Object obj, Object... args) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException +{ + if (!override) { + if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { + Class caller = Reflection.getCallerClass(); + checkAccess(caller, clazz, obj, modifiers); + } + } + MethodAccessor ma = methodAccessor; // read volatile + if (ma == null) { + ma = acquireMethodAccessor(); + } + return ma.invoke(obj, args); +} +``` + +两个嵌套的 if 语句是用来进行权限检查的。 + +`invoke()` 方法实际上是委派给 MethodAccessor 接口来完成的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/fanshe/fanshe-01.png) + +MethodAccessor 接口有三个实现类,其中的 MethodAccessorImpl 是一个抽象类,另外两个具体的实现类继承了这个抽象类。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/fanshe/fanshe-02.png) + +- NativeMethodAccessorImpl:通过本地方法来实现反射调用; +- DelegatingMethodAccessorImpl:通过委派模式来实现反射调用; + +通过 debug 的方式进入 `invoke()` 方法后,可以看到第一次反射调用会生成一个委派实现 DelegatingMethodAccessorImpl,它在生成的时候会传递一个本地实现 NativeMethodAccessorImpl。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/fanshe/fanshe-03.png) + +也就是说,`invoke()` 方法在执行的时候,会先调用 DelegatingMethodAccessorImpl,然后调用 NativeMethodAccessorImpl,最后再调用实际的方法。 + +“为什么不直接调用本地实现呢?”三妹问。 + +“之所以采用委派实现,是为了能够在本地实现和动态实现之间切换。动态实现是另外一种反射调用机制,它是通过生成字节码的形式来实现的。如果反射调用的次数比较多,动态实现的效率就会更高,因为本地实现需要经过 Java 到 C/C++ 再到 Java 之间的切换过程,而动态实现不需要;但如果反射调用的次数比较少,反而本地实现更快一些。”我说。 + +“那临界点是多少呢?”三妹问。 + +“默认是 15 次。”我说,“可以通过 `-Dsun.reflect.inflationThreshold` 参数类调整。” + +来看下面这个例子。 + +```java +Method setAgeMethod = clazz.getMethod("setAge", int.class); +for (int i = 0;i < 20; i++) { + setAgeMethod.invoke(object, 18); +} +``` + +在 `invoke()` 方法处加断点进入 debug 模式,当 i = 15 的时候,也就是第 16 次执行的时候,会进入到 if 条件分支中,改变 DelegatingMethodAccessorImpl 的委派模式 delegate 为 `(MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod()`,而之前的委派模式 delegate 为 NativeMethodAccessorImpl。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/fanshe/fanshe-04.png) + +“这下明白了吧?三妹。”我说,“接下来,我们再来熟悉一下反射当中常用的 API。” + +**1)获取反射类的 Class 对象** + +`Class.forName()`,参数为反射类的完全限定名。 + +```java +Class c1 = Class.forName("com.itwanger.s39.ReflectionDemo3"); +System.out.println(c1.getCanonicalName()); + +Class c2 = Class.forName("[D"); +System.out.println(c2.getCanonicalName()); + +Class c3 = Class.forName("[[Ljava.lang.String;"); +System.out.println(c3.getCanonicalName()); +``` + +来看一下输出结果: + +``` +com.itwanger.s39.ReflectionDemo3 +double[] +java.lang.String[][] +``` + +类名 + `.class`,只适合在编译前就知道操作的 Class。。 + +```java +Class c1 = ReflectionDemo3.class; +System.out.println(c1.getCanonicalName()); + +Class c2 = String.class; +System.out.println(c2.getCanonicalName()); + +Class c3 = int[][][].class; +System.out.println(c3.getCanonicalName()); +``` + +来看一下输出结果: + +```java +com.itwanger.s39.ReflectionDemo3 +java.lang.String +int[][][] +``` + +**2)创建反射类的对象** + +通过反射来创建对象的方式有两种: + +- 用 Class 对象的 `newInstance()` 方法。 +- 用 Constructor 对象的 `newInstance()` 方法。 + +```java +Class c1 = Writer.class; +Writer writer = (Writer) c1.newInstance(); + +Class c2 = Class.forName("com.itwanger.s39.Writer"); +Constructor constructor = c2.getConstructor(); +Object object = constructor.newInstance(); +``` + +**3)获取构造方法** + +Class 对象提供了以下方法来获取构造方法 Constructor 对象: + +- `getConstructor()`:返回反射类的特定 public 构造方法,可以传递参数,参数为构造方法参数对应 Class 对象;缺省的时候返回默认构造方法。 +- `getDeclaredConstructor()`:返回反射类的特定构造方法,不限定于 public 的。 +- `getConstructors()`:返回类的所有 public 构造方法。 +- `getDeclaredConstructors()`:返回类的所有构造方法,不限定于 public 的。 + +```java +Class c2 = Class.forName("com.itwanger.s39.Writer"); +Constructor constructor = c2.getConstructor(); + +Constructor[] constructors1 = String.class.getDeclaredConstructors(); +for (Constructor c : constructors1) { + System.out.println(c); +} +``` + +**4)获取字段** + +大体上和获取构造方法类似,把关键字 Constructor 换成 Field 即可。 + +```java +Method setNameMethod = clazz.getMethod("setName", String.class); +Method getNameMethod = clazz.getMethod("getName"); +``` + +**5)获取方法** + +大体上和获取构造方法类似,把关键字 Constructor 换成 Method 即可。 + +```java +Method[] methods1 = System.class.getDeclaredMethods(); +Method[] methods2 = System.class.getMethods(); +``` + +“注意,三妹,如果你想反射访问私有字段和(构造)方法的话,需要使用 `Constructor/Field/Method.setAccessible(true)` 来绕开 Java 语言的访问限制。”我说。 + +“好的,二哥。还有资料可以参考吗?”三妹问。 + +“有的,有两篇文章写得非常不错,你在学习反射的时候可以作为参考。”我说。 + +第一篇:深入理解 Java 反射和动态代理 + +>链接:https://dunwu.github.io/javacore/basics/java-reflection.html#_1-%E5%8F%8D%E5%B0%84%E7%AE%80%E4%BB%8B + +第二篇:大白话说Java反射:入门、使用、原理: + +>链接:https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/basic-extra-meal/generic.md b/docs/basic-extra-meal/generic.md new file mode 100644 index 0000000000..a92ba6ac24 --- /dev/null +++ b/docs/basic-extra-meal/generic.md @@ -0,0 +1,468 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 深入理解Java泛型 + +“二哥,为什么要设计泛型啊?”三妹开门见山地问。 + +“三妹啊,听哥慢慢给你讲啊。”我说。 + +Java 在 1.5 时增加了泛型机制,据说专家们为此花费了 5 年左右的时间(听起来很不容易)。有了泛型之后,尤其是对集合类的使用,就变得更规范了。 + +看下面这段简单的代码。 + +```java +ArrayList list = new ArrayList(); +list.add("沉默王二"); +String str = list.get(0); +``` + +“三妹,你能想象到在没有泛型之前该怎么办吗?” + +“嗯,想不到,还是二哥你说吧。” + +嗯,我们可以使用 Object 数组来设计 `Arraylist` 类。 + +```java +class Arraylist { + private Object[] objs; + private int i = 0; + public void add(Object obj) { + objs[i++] = obj; + } + + public Object get(int i) { + return objs[i]; + } +} +``` + +然后,我们向 `Arraylist` 中存取数据。 + +```java +Arraylist list = new Arraylist(); +list.add("沉默王二"); +list.add(new Date()); +String str = (String)list.get(0); +``` + +“三妹,你有没有发现这两个问题?” + +- Arraylist 可以存放任何类型的数据(既可以存字符串,也可以混入日期),因为所有类都继承自 Object 类。 +- 从 Arraylist 取出数据的时候需要强制类型转换,因为编译器并不能确定你取的是字符串还是日期。 + +“嗯嗯,是的呢。”三妹说。 + +对比一下,你就能明显地感受到泛型的优秀之处:使用**类型参数**解决了元素的不确定性——参数类型为 String 的集合中是不允许存放其他类型元素的,取出数据的时候也不需要强制类型转换了。 + +“二哥,那怎么才能设计一个泛型呢?” + +“三妹啊,你一个小白只要会用泛型就行了,还想设计泛型啊?!不过,既然你想了解,那么哥义不容辞。” + + + +首先,我们来按照泛型的标准重新设计一下 `Arraylist` 类。 + +```java +class Arraylist { + private Object[] elementData; + private int size = 0; + + public Arraylist(int initialCapacity) { + this.elementData = new Object[initialCapacity]; + } + + public boolean add(E e) { + elementData[size++] = e; + return true; + } + + E elementData(int index) { + return (E) elementData[index]; + } +} +``` + +一个泛型类就是具有一个或多个类型变量的类。Arraylist 类引入的类型变量为 E(Element,元素的首字母),使用尖括号 `<>` 括起来,放在类名的后面。 + +然后,我们可以用具体的类型(比如字符串)替换类型变量来实例化泛型类。 + +```java +Arraylist list = new Arraylist(); +list.add("沉默王三"); +String str = list.get(0); +``` + +Date 类型也可以的。 + +```java +Arraylist list = new Arraylist(); +list.add(new Date()); +Date date = list.get(0); +``` + +其次,我们还可以在一个非泛型的类(或者泛型类)中定义泛型方法。 + +```java +class Arraylist { + public T[] toArray(T[] a) { + return (T[]) Arrays.copyOf(elementData, size, a.getClass()); + } +} +``` + +不过,说实话,泛型方法的定义看起来略显晦涩。来一副图吧(注意:方法返回类型和方法参数类型至少需要一个)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/generic/generic-01.png) + +现在,我们来调用一下泛型方法。 + +```java +Arraylist list = new Arraylist<>(4); +list.add("沉"); +list.add("默"); +list.add("王"); +list.add("二"); + +String [] strs = new String [4]; +strs = list.toArray(strs); + +for (String str : strs) { + System.out.println(str); +} +``` + +然后,我们再来说说泛型变量的限定符 `extends`。 + +在解释这个限定符之前,我们假设有三个类,它们之间的定义是这样的。 + +```java +class Wanglaoer { + public String toString() { + return "王老二"; + } +} + +class Wanger extends Wanglaoer{ + public String toString() { + return "王二"; + } +} + +class Wangxiaoer extends Wanger{ + public String toString() { + return "王小二"; + } +} +``` + +我们使用限定符 `extends` 来重新设计一下 `Arraylist` 类。 + +```java +class Arraylist { +} +``` + +当我们向 `Arraylist` 中添加 `Wanglaoer` 元素的时候,编译器会提示错误:`Arraylist` 只允许添加 `Wanger` 及其子类 `Wangxiaoer` 对象,不允许添加其父类 `Wanglaoer`。 + +```java +Arraylist list = new Arraylist<>(3); +list.add(new Wanger()); +list.add(new Wanglaoer()); +// The method add(Wanger) in the type Arraylist is not applicable for the arguments +// (Wanglaoer) +list.add(new Wangxiaoer()); +``` + +也就是说,限定符 `extends` 可以缩小泛型的类型范围。 + +“哦,明白了。”三妹若有所思的点点头,“二哥,听说虚拟机没有泛型?” + +“三妹,你功课做得可以啊。哥可以肯定地回答你,虚拟机是没有泛型的。” + +“怎么确定虚拟机有没有泛型呢?”三妹问。 + +“只要我们把泛型类的字节码进行反编译就看到了!”用反编译工具将 class 文件反编译后,我说,“三妹,你看。” + +```java +// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. +// Jad home page: http://www.kpdus.com/jad.html +// Decompiler options: packimports(3) +// Source File Name: Arraylist.java + +package com.cmower.java_demo.fanxing; + +import java.util.Arrays; + +class Arraylist +{ + + public Arraylist(int initialCapacity) + { + size = 0; + elementData = new Object[initialCapacity]; + } + + public boolean add(Object e) + { + elementData[size++] = e; + return true; + } + + Object elementData(int index) + { + return elementData[index]; + } + + private Object elementData[]; + private int size; +} +``` + +类型变量 `` 消失了,取而代之的是 Object ! + +“既然如此,那如果泛型类使用了限定符 `extends`,结果会怎么样呢?”三妹这个问题问的很巧妙。 + +来看这段代码。 + +```java +class Arraylist2 { + private Object[] elementData; + private int size = 0; + + public Arraylist2(int initialCapacity) { + this.elementData = new Object[initialCapacity]; + } + + public boolean add(E e) { + elementData[size++] = e; + return true; + } + + E elementData(int index) { + return (E) elementData[index]; + } +} +``` + +反编译后的结果如下。 + +```java +// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. +// Jad home page: http://www.kpdus.com/jad.html +// Decompiler options: packimports(3) +// Source File Name: Arraylist2.java + +package com.cmower.java_demo.fanxing; + + +// Referenced classes of package com.cmower.java_demo.fanxing: +// Wanger + +class Arraylist2 +{ + + public Arraylist2(int initialCapacity) + { + size = 0; + elementData = new Object[initialCapacity]; + } + + public boolean add(Wanger e) + { + elementData[size++] = e; + return true; + } + + Wanger elementData(int index) + { + return (Wanger)elementData[index]; + } + + private Object elementData[]; + private int size; +} +``` + +“你看,类型变量 `` 不见了,E 被替换成了 `Wanger`”,我说,“通过以上两个例子说明,Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 `Object`)” + +“二哥,类型擦除会有什么问题吗?”三妹又问了一个很有水平的问题。 + +“三妹啊,你还别说,类型擦除真的会有一些问题。”我说,“来看一下这段代码。” + +```java +public class Cmower { + + public static void method(Arraylist list) { + System.out.println("Arraylist list"); + } + + public static void method(Arraylist list) { + System.out.println("Arraylist list"); + } + +} +``` + +在浅层的意识上,我们会想当然地认为 `Arraylist list` 和 `Arraylist list` 是两种不同的类型,因为 String 和 Date 是不同的类。 + +但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”): + +``` +>Erasure of method method(Arraylist) is the same as another method in type + Cmower +> +>Erasure of method method(Arraylist) is the same as another method in type + Cmower +``` + + +大致的意思就是,这两个方法的参数类型在擦除后是相同的。 + +也就是说,`method(Arraylist list)` 和 `method(Arraylist list)` 是同一种参数类型的方法,不能同时存在。类型变量 `String` 和 `Date` 在擦除后会自动消失,method 方法的实际参数是 `Arraylist list`。 + +有句俗话叫做:“百闻不如一见”,但即使见到了也未必为真——泛型的擦除问题就可以很好地佐证这个观点。 + +“哦,明白了。二哥,听说泛型还有通配符?” + +“三妹啊,哥突然觉得你很适合作一枚可爱的程序媛啊!你这预习的功课做得可真到家啊,连通配符都知道!” + +通配符使用英文的问号(?)来表示。在我们创建一个泛型对象时,可以使用关键字 `extends` 限定子类,也可以使用关键字 `super` 限定父类。 + +我们来看下面这段代码。 + +```java +class Arraylist { + private Object[] elementData; + private int size = 0; + + public Arraylist(int initialCapacity) { + this.elementData = new Object[initialCapacity]; + } + + public boolean add(E e) { + elementData[size++] = e; + return true; + } + + public E get(int index) { + return (E) elementData[index]; + } + + public int indexOf(Object o) { + if (o == null) { + for (int i = 0; i < size; i++) + if (elementData[i]==null) + return i; + } else { + for (int i = 0; i < size; i++) + if (o.equals(elementData[i])) + return i; + } + return -1; + } + + public boolean contains(Object o) { + return indexOf(o) >= 0; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + + for (Object o : elementData) { + if (o != null) { + E e = (E)o; + sb.append(e.toString()); + sb.append(',').append(' '); + } + } + return sb.toString(); + } + + public int size() { + return size; + } + + public E set(int index, E element) { + E oldValue = (E) elementData[index]; + elementData[index] = element; + return oldValue; + } +} +``` + +1)新增 `indexOf(Object o)` 方法,判断元素在 `Arraylist` 中的位置。注意参数为 `Object` 而不是泛型 `E`。 + +2)新增 `contains(Object o)` 方法,判断元素是否在 `Arraylist` 中。注意参数为 `Object` 而不是泛型 `E`。 + +3)新增 `toString()` 方法,方便对 `Arraylist` 进行打印。 + +4)新增 `set(int index, E element)` 方法,方便对 `Arraylist` 元素的更改。 + +因为泛型擦除的原因,`Arraylist list = new Arraylist();` 这样的语句是无法通过编译的,尽管 Wangxiaoer 是 Wanger 的子类。但如果我们确实需要这种 “向上转型” 的关系,该怎么办呢?这时候就需要通配符来发挥作用了。 + +利用 `` 形式的通配符,可以实现泛型的向上转型,来看例子。 + +```java +Arraylist list2 = new Arraylist<>(4); +list2.add(null); +// list2.add(new Wanger()); +// list2.add(new Wangxiaoer()); + +Wanger w2 = list2.get(0); +// Wangxiaoer w3 = list2.get(1); +``` + +list2 的类型是 `Arraylist`,翻译一下就是,list2 是一个 `Arraylist`,其类型是 `Wanger` 及其子类。 + +注意,“关键”来了!list2 并不允许通过 `add(E e)` 方法向其添加 `Wanger` 或者 `Wangxiaoer` 的对象,唯一例外的是 `null`。 + +“那就奇了怪了,既然不让存放元素,那要 `Arraylist` 这样的 list2 有什么用呢?”三妹好奇地问。 + +虽然不能通过 `add(E e)` 方法往 list2 中添加元素,但可以给它赋值。 + +```java +Arraylist list = new Arraylist<>(4); + +Wanger wanger = new Wanger(); +list.add(wanger); + +Wangxiaoer wangxiaoer = new Wangxiaoer(); +list.add(wangxiaoer); + +Arraylist list2 = list; + +Wanger w2 = list2.get(1); +System.out.println(w2); + +System.out.println(list2.indexOf(wanger)); +System.out.println(list2.contains(new Wangxiaoer())); +``` + +`Arraylist list2 = list;` 语句把 list 的值赋予了 list2,此时 `list2 == list`。由于 list2 不允许往其添加其他元素,所以此时它是安全的——我们可以从容地对 list2 进行 `get()`、`indexOf()` 和 `contains()`。想一想,如果可以向 list2 添加元素的话,这 3 个方法反而变得不太安全,它们的值可能就会变。 + +利用 `` 形式的通配符,可以向 Arraylist 中存入父类是 `Wanger` 的元素,来看例子。 + +```java +Arraylist list3 = new Arraylist<>(4); +list3.add(new Wanger()); +list3.add(new Wangxiaoer()); + +// Wanger w3 = list3.get(0); +``` + +需要注意的是,无法从 `Arraylist` 这样类型的 list3 中取出数据。 + +“三妹,关于泛型,这里还有一篇很不错的文章,你等会去看一下。”我说。 + +>https://www.pdai.tech/md/java/basic/java-basic-x-generic.html + +“对泛型机制讲的也很透彻,你结合二哥给你讲的这些,再深入的学习一下。” + +“好的,二哥。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/hashcode.md b/docs/basic-extra-meal/hashcode.md new file mode 100644 index 0000000000..d481896064 --- /dev/null +++ b/docs/basic-extra-meal/hashcode.md @@ -0,0 +1,234 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 深入理解Java中的hashCode方法 + +假期结束了,需要快速切换到工作的状态投入到新的一天当中。放假的时候痛快地玩耍,上班的时候积极的工作,这应该是我们大多数“现代人”该有的生活状态。 + +我之所以费尽心思铺垫了前面这段话,就是想告诉大家,技术文虽迟但到,来吧,学起来~ + +今天我们来谈谈 Java 中的 `hashCode()` 方法。众所周知,Java 是一门面向对象的编程语言,所有的类都会默认继承自 Object 类,而 Object 的中文意思就是“对象”。 + +Object 类中就包含了 `hashCode()` 方法: + +```java +@HotSpotIntrinsicCandidate +public native int hashCode(); +``` + +意味着所有的类都会有一个 `hashCode()` 方法,该方法会返回一个 int 类型的值。由于 `hashCode()` 方法是一个本地方法(`native` 关键字修饰的方法,用 `C/C++` 语言实现,由 Java 调用),意味着 Object 类中并没有给出具体的实现。 + +具体的实现可以参考 `jdk/src/hotspot/share/runtime/synchronizer.cpp`(源码可以到 GitHub 上 OpenJDK 的仓库中下载)。`get_next_hash()` 方法会根据 hashCode 的取值来决定采用哪一种哈希值的生成策略。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/hashcode-1.png) + +并且 `hashCode()` 方法被 `@HotSpotIntrinsicCandidate` 注解修饰,说明它在 HotSpot 虚拟机中有一套高效的实现,基于 CPU 指令。 + +那大家有没有想过这样一个问题:为什么 Object 类需要一个 `hashCode()` 方法呢? + +在 Java 中,`hashCode()` 方法的主要作用就是为了配合哈希表使用的。 + +哈希表(Hash Table),也叫散列表,是一种可以通过关键码值(key-value)直接访问的数据结构,它最大的特点就是可以快速实现查找、插入和删除。其中用到的算法叫做哈希,就是把任意长度的输入,变换成固定长度的输出,该输出就是哈希值。像 MD5、SHA1 都用的是哈希算法。 + +像 Java 中的 HashSet、Hashtable(注意是小写的 t)、HashMap 都是基于哈希表的具体实现。其中的 HashMap 就是最典型的代表,不仅面试官经常问,工作中的使用频率也非常的高。 + +大家想一下,如果没有哈希表,但又需要这样一个数据结构,它里面存放的数据是不允许重复的,该怎么办呢? + +要不使用 `equals()` 方法进行逐个比较?这种方案当然是可行的。但如果数据量特别特别大,采用 `equals()` 方法进行逐个对比的效率肯定很低很低,最好的解决方案就是哈希表。 + +拿 HashMap 来说吧。当我们要在它里面添加对象时,先调用这个对象的 `hashCode()` 方法,得到对应的哈希值,然后将哈希值和对象一起放到 HashMap 中。当我们要再添加一个新的对象时: + +- 获取对象的哈希值; +- 和之前已经存在的哈希值进行比较,如果不相等,直接存进去; +- 如果有相等的,再调用 `equals()` 方法进行对象之间的比较,如果相等,不存了; +- 如果不等,说明哈希冲突了,增加一个链表,存放新的对象; +- 如果链表的长度大于 8,转为红黑树来处理。 + +就这么一套下来,调用 `equals()` 方法的频率就大大降低了。也就是说,只要哈希算法足够的高效,把发生哈希冲突的频率降到最低,哈希表的效率就特别的高。 + +来看一下 HashMap 的哈希算法: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +先调用对象的 `hashCode()` 方法,然后对该值进行右移运算,然后再进行异或运算。 + +通常来说,String 会用来作为 HashMap 的键进行哈希运算,因此我们再来看一下 String 的 `hashCode()` 方法: + +```java +public int hashCode() { + int h = hash; + if (h == 0 && value.length > 0) { + hash = h = isLatin1() ? StringLatin1.hashCode(value) + : StringUTF16.hashCode(value); + } + return h; +} +public static int hashCode(byte[] value) { + int h = 0; + int length = value.length >> 1; + for (int i = 0; i < length; i++) { + h = 31 * h + getChar(value, i); + } + return h; +} +``` + +可想而知,经过这么一系列复杂的运算,再加上 JDK 作者这种大师级别的设计,哈希冲突的概率我相信已经降到了最低。 + +当然了,从理论上来说,对于两个不同对象,它们通过 `hashCode()` 方法计算后的值可能相同。因此,不能使用 `hashCode()` 方法来判断两个对象是否相等,必须得通过 `equals()` 方法。 + +也就是说: + +- 如果两个对象调用 `equals()` 方法得到的结果为 true,调用 `hashCode()` 方法得到的结果必定相等; +- 如果两个对象调用 `hashCode()` 方法得到的结果不相等,调用 `equals()` 方法得到的结果必定为 false; + +反之: + +- 如果两个对象调用 `equals()` 方法得到的结果为 false,调用 `hashCode()` 方法得到的结果不一定不相等; +- 如果两个对象调用 `hashCode()` 方法得到的结果相等,调用 `equals()` 方法得到的结果不一定为 true; + +来看下面这段代码。 + +```java +public class Test { + public static void main(String[] args) { + Student s1 = new Student(18, "张三"); + Map scores = new HashMap<>(); + scores.put(s1, 98); + System.out.println(scores.get(new Student(18, "张三"))); + } +} + class Student { + private int age; + private String name; + + public Student(int age, String name) { + this.age = age; + this.name = name; + } + + @Override + public boolean equals(Object o) { + Student student = (Student) o; + return age == student.age && + Objects.equals(name, student.name); + } + } +``` + +我们重写了 Student 类的 `equals()` 方法,如果两个学生的年纪和姓名相同,我们就认为是同一个学生,虽然很离谱,但我们就是这么草率。 + +在 `main()` 方法中,18 岁的张三考试得了 98 分,很不错的成绩,我们把张三和成绩放到了 HashMap 中,然后准备输出张三的成绩: + +``` +null +``` + +很不巧,结果为 null,而不是预期当中的 98。这是为什么呢? + +原因就在于重写 `equals()` 方法的时候没有重写 `hashCode()` 方法。默认情况下,`hashCode()` 方法是一个本地方法,会返回对象的存储地址,显然 `put()` 中的 s1 和 `get()` 中的 `new Student(18, "张三")` 是两个对象,它们的存储地址肯定是不同的。 + +HashMap 的 `get()` 方法会调用 `hash(key.hashCode())` 计算对象的哈希值,虽然两个不同的 `hashCode()` 结果经过 `hash()` 方法计算后有可能得到相同的结果,但这种概率微乎其微,所以就导致 `scores.get(new Student(18, "张三"))` 无法得到预期的值 18。 + +怎么解决这个问题呢?很简单,重写 `hashCode()` 方法。 + +```java + @Override + public int hashCode() { + return Objects.hash(age, name); + } +``` + +Objects 类的 `hash()` 方法可以针对不同数量的参数生成新的 `hashCode()` 值。 + +```java +public static int hashCode(Object a[]) { + if (a == null) + return 0; + + int result = 1; + + for (Object element : a) + result = 31 * result + (element == null ? 0 : element.hashCode()); + + return result; +} +``` + +代码似乎很简单,归纳出的数学公式如下所示(n 为字符串长度)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/hashcode-2.png) + +注意:31 是个奇质数,不大不小,一般质数都非常适合哈希计算,偶数相当于移位运算,容易溢出,造成数据信息丢失。 + +这就意味着年纪和姓名相同的情况下,会得到相同的哈希值。`scores.get(new Student(18, "张三"))` 就会返回 98 的预期值了。 + +《Java 编程思想》这本圣经中有一段话,对 `hashCode()` 方法进行了一段描述。 + +>设计 `hashCode()` 时最重要的因素就是:无论何时,对同一个对象调用 `hashCode()` 都应该生成同样的值。如果在将一个对象用 `put()` 方法添加进 HashMap 时产生一个 `hashCode()` 值,而用 `get()` 方法取出时却产生了另外一个 `hashCode()` 值,那么就无法重新取得该对象了。所以,如果你的 `hashCode()` 方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,`hashCode()` 就会生成一个不同的哈希值,相当于产生了一个不同的键。 + +也就是说,如果在重写 `hashCode()` 和 `equals()` 方法时,对象中某个字段容易发生改变,那么最好舍弃这些字段,以免产生不可预期的结果。 + +好。有了上面这些内容作为基础后,我们回头再来看看本地方法 `hashCode()` 的 C++ 源码。 + +```java +static inline intptr_t get_next_hash(Thread* current, oop obj) { + intptr_t value = 0; + if (hashCode == 0) { + // This form uses global Park-Miller RNG. + // On MP system we'll have lots of RW access to a global, so the + // mechanism induces lots of coherency traffic. + value = os::random(); + } else if (hashCode == 1) { + // This variation has the property of being stable (idempotent) + // between STW operations. This can be useful in some of the 1-0 + // synchronization schemes. + intptr_t addr_bits = cast_from_oop(obj) >> 3; + value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random; + } else if (hashCode == 2) { + value = 1; // for sensitivity testing + } else if (hashCode == 3) { + value = ++GVars.hc_sequence; + } else if (hashCode == 4) { + value = cast_from_oop(obj); + } else { + // Marsaglia's xor-shift scheme with thread-specific state + // This is probably the best overall implementation -- we'll + // likely make this the default in future releases. + unsigned t = current->_hashStateX; + t ^= (t << 11); + current->_hashStateX = current->_hashStateY; + current->_hashStateY = current->_hashStateZ; + current->_hashStateZ = current->_hashStateW; + unsigned v = current->_hashStateW; + v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)); + current->_hashStateW = v; + value = v; + } + + value &= markWord::hash_mask; + if (value == 0) value = 0xBAD; + assert(value != markWord::no_hash, "invariant"); + return value; +} +``` + +如果没有 C++ 基础的话,不用细致去看每一行代码,我们只通过表面去了解一下 `get_next_hash()` 这个方法就行。其中的 `hashCode` 变量是 JVM 启动时的一个全局参数,可以通过它来切换哈希值的生成策略。 + +- `hashCode==0`,调用操作系统 OS 的 `random()` 方法返回随机数。 +- `hashCode == 1`,在 STW(stop-the-world)操作中,这种策略通常用于同步方案中。利用对象地址进行计算,使用不经常更新的随机数(`GVars.stw_random`)参与其中。 +- `hashCode == 2`,使用返回 1,用于某些情况下的测试。 +- `hashCode == 3`,从 0 开始计算哈希值,不是线程安全的,多个线程可能会得到相同的哈希值。 +- `hashCode == 4`,与创建对象的内存位置有关,原样输出。 +- `hashCode == 5`,默认值,支持多线程,使用了 Marsaglia 的 xor-shift 算法产生伪随机数。所谓的 xor-shift 算法,简单来说,看起来就是一个移位寄存器,每次移入的位由寄存器中若干位取异或生成。所谓的伪随机数,不是完全随机的,但是真随机生成比较困难,所以只要能通过一定的随机数统计检测,就可以当作真随机数来使用。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/immutable.md b/docs/basic-extra-meal/immutable.md new file mode 100644 index 0000000000..3be63eae23 --- /dev/null +++ b/docs/basic-extra-meal/immutable.md @@ -0,0 +1,204 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 深入理解Java中的不可变对象 + +>二哥,你能给我说说为什么 String 是 immutable 类(不可变对象)吗?我想研究它,想知道为什么它就不可变了,这种强烈的愿望就像想研究浩瀚的星空一样。但无奈自身功力有限,始终觉得雾里看花终隔一层。二哥你的文章总是充满趣味性,我想一定能够说明白,我也一定能够看明白,能在接下来写一写吗? + +收到读者小 R 的私信后,我就总感觉自己有一种义不容辞的责任,非要把 immutable 类说明白不可! + + +### 01、什么是不可变类 + +一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何 setter 方法供外部类去修改。 + +还记得《神雕侠侣》中小龙女的古墓吗?随着那一声巨响,仅有的通道就被无情地关闭了。别较真那个密道,我这么说只是为了打开你的想象力,让你对不可变类有一个更直观的印象。 + +自从有了多线程,生产力就被无限地放大了,所有的程序员都爱它,因为强大的硬件能力被充分地利用了。但与此同时,所有的程序员都对它心生忌惮,因为一不小心,多线程就会把对象的状态变得混乱不堪。 + +为了保护状态的原子性、可见性、有序性,我们程序员可以说是竭尽所能。其中,synchronized(同步)关键字是最简单最入门的一种解决方案。 + +假如说类是不可变的,那么对象的状态就也是不可变的。这样的话,每次修改对象的状态,就会产生一个新的对象供不同的线程使用,我们程序员就不必再担心并发问题了。 + +### 02、常见的不可变类 + +提到不可变类,几乎所有的程序员第一个想到的,就是 String 类。那为什么 String 类要被设计成不可变的呢? + +1)常量池的需要 + +字符串常量池是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串在常量池中不存在,那么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。 + +2)hashCode 的需要 + +因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比如说作为 HashMap 的键),多次调用只返回同一个值,来提高效率。 + +3)线程安全 + +就像之前说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。 + +因此,当我们调用 String 类的任何方法(比如说 `trim()`、`substring()`、`toLowerCase()`)时,总会返回一个新的对象,而不影响之前的值。 + +```java +String cmower = "沉默王二,一枚有趣的程序员"; +cmower.substring(0,4); +System.out.println(cmower);// 沉默王二,一枚有趣的程序员 +``` + +虽然调用 `substring()` 方法对 cmower 进行了截取,但 cmower 的值没有改变。 + +除了 String 类,包装器类 Integer、Long 等也是不可变类。 + +### 03、手撸不可变类 + +看懂一个不可变类也许容易,但要创建一个自定义的不可变类恐怕就有点难了。但知难而进是我们作为一名优秀的程序员不可或缺的品质,正因为不容易,我们才能真正地掌握它。 + +接下来,就请和我一起,来自定义一个不可变类吧。一个不可变诶,必须要满足以下 4 个条件: + +1)确保类是 final 的,不允许被其他类继承。 + +2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。 + +3)不要提供任何 setter 方法。 + +4)如果要修改类的状态,必须返回一个新的对象。 + +按照以上条件,我们来自定义一个简单的不可变类 Writer。 + +```java +public final class Writer { + private final String name; + private final int age; + + public Writer(String name, int age) { + this.name = name; + this.age = age; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } +} +``` + +Writer 类是 final 的,name 和 age 也是 final 的,没有 setter 方法。 + +OK,据说这个作者分享了很多博客,广受读者的喜爱,因此某某出版社找他写了一本书(Book)。Book 类是这样定义的: + +```java +public class Book { + private String name; + private int price; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + @Override + public String toString() { + return "Book{" + + "name='" + name + '\'' + + ", price=" + price + + '}'; + } +} +``` + +2 个字段,分别是 name 和 price,以及 getter 和 setter,重写后的 `toString()` 方法。然后,在 Writer 类中追加一个可变对象字段 book。 + +```java +public final class Writer { + private final String name; + private final int age; + private final Book book; + + public Writer(String name, int age, Book book) { + this.name = name; + this.age = age; + this.book = book; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + + public Book getBook() { + return book; + } +} +``` + +并在构造方法中追加了 Book 参数,以及 Book 的 getter 方法。 + +完成以上工作后,我们来新建一个测试类,看看 Writer 类的状态是否真的不可变。 + +```java +public class WriterDemo { + public static void main(String[] args) { + Book book = new Book(); + book.setName("Web全栈开发进阶之路"); + book.setPrice(79); + + Writer writer = new Writer("沉默王二",18, book); + System.out.println("定价:" + writer.getBook()); + writer.getBook().setPrice(59); + System.out.println("促销价:" + writer.getBook()); + } +} +``` + +程序输出的结果如下所示: + +```java +定价:Book{name='Web全栈开发进阶之路', price=79} +促销价:Book{name='Web全栈开发进阶之路', price=59} +``` + +糟糕,Writer 类的不可变性被破坏了,价格发生了变化。为了解决这个问题,我们需要为不可变类的定义规则追加一条内容: + +如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本。也就是说,Writer 类中的 `getBook()` 方法应该修改为: + +```java +public Book getBook() { + Book clone = new Book(); + clone.setPrice(this.book.getPrice()); + clone.setName(this.book.getName()); + return clone; +} +``` + +这样的话,构造方法初始化后的 Book 对象就不会再被修改了。此时,运行 WriterDemo,就会发现价格不再发生变化了。 + +``` +定价:Book{name='Web全栈开发进阶之路', price=79} +促销价:Book{name='Web全栈开发进阶之路', price=79} +``` + +### 04、总结 + +不可变类有很多优点,就像之前提到的 String 类那样,尤其是在多线程环境下,它非常的安全。尽管每次修改都会创建一个新的对象,增加了内存的消耗,但这个缺点相比它带来的优点,显然是微不足道的——无非就是捡了西瓜,丢了芝麻。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-extra-meal/instanceof-jvm.md b/docs/basic-extra-meal/instanceof-jvm.md new file mode 100644 index 0000000000..a3f4241b8a --- /dev/null +++ b/docs/basic-extra-meal/instanceof-jvm.md @@ -0,0 +1,110 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# instanceof关键字是如何实现的? + +小二那天去面试,碰到了这个问题:“**instanceof 关键字是如何实现的**?”面试官希望他能从底层来分析一下,结果小二没答上来,就来问我。 + +我唯唯诺诺,强装镇定,只好把 R 大的一篇回答甩给了他,并且叮嘱他:“认认真真看,玩完后要是还不明白,再来问我。。。” + +>作者:RednaxelaFX,整理:沉默王二,链接:https://www.zhihu.com/question/21574535/answer/18998914 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/instanceof-jvm-b676fee6-bfd4-4ae9-9c7b-e488e345f775.gif) + +-------- + +### 场景一:月薪 3000 元一下的码农职位 + +用 Java 伪代码来表现instanceof关键字在Java语言规范所描述的运行时语义,是这样的: + +```java +// obj instanceof T +boolean result; +if (obj == null) { + result = false; +} else { + try { + T temp = (T) obj; // checkcast + result = true; + } catch (ClassCastException e) { + result = false; + } +} +``` + +用中文说就是:如果有表达式 `obj instanceof T`,那么如果 obj 不为 null 并且 (T) obj 不抛 ClassCastException 异常则该表达式值为 true ,否则值为 false 。 + +如果面试官说“这不是废话嘛”,进入场景二。 + +### 场景二:月薪6000-8000的Java研发职位 + +JVM有一条名为 instanceof 的指令,而Java源码编译到Class文件时会把Java语言中的 instanceof 运算符映射到JVM的 instanceof 指令上。 + +javac是这样做的: + +- instanceof 是javac能识别的一个关键字,对应到Token.INSTANCEOF的token类型。做词法分析的时候扫描到"instanceof"关键字就映射到了一个Token.INSTANCEOF token。 +- 该编译器的抽象语法树节点有一个JCTree.JCInstanceOf类用于表示instanceof运算。做语法分析的时候解析到[instanceof运算符](https://tobebetterjavaer.com/oo/instanceof.html)就会生成这个JCTree.JCInstanceof类型的节点。 +- 中途还得根据Java语言规范对instanceof运算符的编译时检查的规定把有问题的情况找出来。 +- 到最后生成字节码的时候为JCTree.JCInstanceof节点生成instanceof字节码指令。 + +回答到这层面就已经能解决好些实际问题了,如果面试官还说,“这不还是废话嘛”,进入场景三。 + +### 场景三:月薪10000的Java高级研发职位 + +先简单介绍一下instanceof的字节码: + +- 操作:确定对象是否为给定的类型 +- 指令格式:instanceof|indexbyte1|indexbyte2 +- 指令执行前后的栈顶状态: + - ……,objectref=> + - ……,result + +再简单描述下:indexbyte1和indexbyte2用于构造对当前类的常量池的索引,objectref为reference类型,可以是某个类,数组的实例或者是接口。 + +基本的实现过程:对indexbyte1和indexbyte2构造的常量池索引进行解析,然后根据java规范判断解析的类是不是objectref的一个实例,最后在栈顶写入结果。 + +基本上就是根据规范来 YY 下实现,就能八九不离十蒙混过关了。 + +如果面试官还不满意,进入场景四。 + +### 场景四:月薪10000以上的Java资深研发职位 + +这个岗位注重性能调优什么的,R 大说可以上论文了: + +>https://dl.acm.org/doi/10.1145/583810.583821 + +论文我也看不懂,所以这里就不 BB 了。(逃 + +篇论文描述了HotSpot VM做子类型判断的算法,这里简单补充一下JDK6至今的HotSpot VM实际采用的算法: + +```java +S.is_subtype_of(T) := { + int off = T.offset; + if (S == T) return true; + if (T == S[off]) return true; + if (off != &cache) return false; + if ( S.scan_secondary_subtype_array(T) ) { + S.cache = T; + return true; + } + return false; +} +``` + +HotSpot VM的两个编译器,Client Compiler (C1) 与 Server Compiler (C2) 各自对子类型判断的实现有更进一步的优化。实际上在JVM里,instanceof的功能就实现了4份,VM runtime、解释器、C1、C2各一份。 + +VM runtime的: + +>http://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/file/tip/src/share/vm/oops/oop.inline.hpp + +分享的最后,二哥简单来说一下。 + +这个问题涉及语法细节,涉及jvm实现,涉及编译器,还涉及一点点数据结构设计,比较考验一个 Java 程序员的内功,如果要回答到论文的程度,那真的是,面试官也得提前备好知识点,不然应聘者的回答啥也听不懂就挺尴尬的。 + +反正 R 大回答里的很多细节我都是第一次听,逃了逃了。。。。。。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/int-cache.md b/docs/basic-extra-meal/int-cache.md new file mode 100644 index 0000000000..e453bed329 --- /dev/null +++ b/docs/basic-extra-meal/int-cache.md @@ -0,0 +1,171 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java中int、Integer、new Integer之间的区别 + +“三妹,今天我们来补一个小的知识点:Java 数据类型缓存池。”我喝了一口枸杞泡的茶后对三妹说,“考你一个问题哈:`new Integer(18) 与 Integer.valueOf(18) ` 的区别是什么?” + +“难道不一样吗?”三妹有点诧异。 + +“不一样的。”我笑着说。 + +- `new Integer(18)` 每次都会新建一个对象; +- `Integer.valueOf(18)` 会使⽤用缓存池中的对象,多次调用只会取同⼀一个对象的引用。 + +来看下面这段代码: + +```java +Integer x = new Integer(18); +Integer y = new Integer(18); +System.out.println(x == y); + +Integer z = Integer.valueOf(18); +Integer k = Integer.valueOf(18); +System.out.println(z == k); + +Integer m = Integer.valueOf(300); +Integer p = Integer.valueOf(300); +System.out.println(m == p); +``` + +来看一下输出结果吧: + +``` +false +true +false +``` + +“第一个 false,我知道原因,因为 new 出来的是不同的对象,地址不同。”三妹解释道,“第二个和第三个我认为都应该是 true 啊,为什么第三个会输出 false 呢?这个我理解不了。” + +“其实原因也很简单。”我胸有成竹地说。 + +基本数据类型的包装类除了 Float 和 Double 之外,其他六个包装器类(Byte、Short、Integer、Long、Character、Boolean)都有常量缓存池。 + +- Byte:-128~127,也就是所有的 byte 值 +- Short:-128~127 +- Long:-128~127 +- Character:\u0000 - \u007F +- Boolean:true 和 false + +拿 Integer 来举例子,Integer 类内部中内置了 256 个 Integer 类型的缓存数据,当使用的数据范围在 -128~127 之间时,会直接返回常量池中数据的引用,而不是创建对象,超过这个范围时会创建新的对象。 + + 18 在 -128~127 之间,300 不在。 + +来看一下 valueOf 方法的源码吧。 + +```java +public static Integer valueOf(int i) { + if (i >=IntegerCache.low && i <=IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +“哦,原来是因为 Integer.IntegerCache 这个内部类的原因啊!”三妹好像发现了新大陆。 + +“是滴。来看一下 IntegerCache 这个静态内部类的源码吧。” + +```java +private static class IntegerCache { + static final int low = -128; + static final int high; + static final Integer cache[]; + + static { + // high value may be configured by property + int h = 127; + String integerCacheHighPropValue = + sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); + if (integerCacheHighPropValue != null) { + try { + int i = parseInt(integerCacheHighPropValue); + i = Math.max(i, 127); + // Maximum array size is Integer.MAX_VALUE + h = Math.min(i, Integer.MAX_VALUE - (-low) -1); + } catch( NumberFormatException nfe) { + // If the property cannot be parsed into an int, ignore it. + } + } + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + + // range [-128, 127] must be interned (JLS7 5.1.7) + assert Integer.IntegerCache.high >= 127; + } + + private IntegerCache() {} +} +``` + + + +之前我们在[学习 static 关键字](https://github.com/itwanger/toBeBetterJavaer/blob/master/docs/keywords/java-static.md)的时候,提到过静态代码块,还记得吧?三妹。静态代码块通常用来初始化一些静态变量,它会优先于 main() 方法执行。 + +在静态代码块中,low 为 -128,也就是缓存池的最小值;high 默认为 127,也就是缓存池的最大值,共计 256 个。 + +*可以在 JVM 启动的时候,通过 `-XX:AutoBoxCacheMax=NNN` 来设置缓存池的大小,当然了,不能无限大,最大到 `Integer.MAX_VALUE -129`* + +之后,初始化 cache 数组的大小,然后遍历填充,下标从 0 开始。 + +“明白了吧?三妹。”我喝了一口水后,扭头看了看旁边的三妹。 + +“这段代码不难理解,难理解的是 `assert Integer.IntegerCache.high >= 127;`,这行代码是干嘛的呀?”三妹很是不解。 + +“哦哦,你挺细心的呀!”三妹真不错,求知欲望越来越强烈了。 + +assert 是 Java 中的一个关键字,寓意是断言,为了方便调试程序,并不是发布程序的组成部分。 + +默认情况下,断言是关闭的,可以在命令行运行 Java 程序的时候加上 `-ea` 参数打开断言。 + +来看这段代码。 + +```java +public class AssertTest { + public static void main(String[] args) { + int high = 126; + assert high >= 127; + } +} +``` + +假设手动设置的缓存池大小为 126,显然不太符合缓存池的预期值 127,结果会输出什么呢? + +直接在 Intellij IDEA 中打开命令行终端,进入 classes 文件,执行: + +``` + /usr/libexec/java_home -v 1.8 --exec java -ea com.itwanger.s51.AssertTest +``` + +*我用的 macOS 环境,装了好多个版本的 JDK,该命令可以切换到 JDK 8* + +也可以不指定 Java 版本直接执行(加上 `-ea` 参数): + +``` +java -ea com.itwanger.s51.AssertTest +``` + +“呀,报错了呀。”三妹喊道。 + +``` +Exception in thread "main" java.lang.AssertionError + at com.itwanger.s51.AssertTest.main(AssertTest.java:9) +``` + +“是滴,因为 126 小于 127。”我回答道。 + +“原来 assert 是这样用的啊,我明白了。”三妹表示学会了。 + +“那,缓存池之所以存在的原因也是因为这样做可以提高程序的整体性能,因为相对来说,比如说 Integer,-128~127 这个范围内的 256 个数字使用的频率会高一点。”我总结道。 + +“get 了!二哥你真棒,又学到了。”三妹很开心~ + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/java-naming.md b/docs/basic-extra-meal/java-naming.md similarity index 82% rename from docs/src/basic-extra-meal/java-naming.md rename to docs/basic-extra-meal/java-naming.md index 10e6e3663a..4dd1cc07aa 100644 --- a/docs/src/basic-extra-meal/java-naming.md +++ b/docs/basic-extra-meal/java-naming.md @@ -1,18 +1,12 @@ --- -title: 5 分钟编码,1 小时命名,笑 -shortTitle: Java命名规范 category: - Java核心 tag: - - Java语法基础 -description: 本文介绍了Java编程中的命名规范,以帮助程序员编写可读性强、易于维护的代码。我们将从变量、方法、类和接口命名等方面讲解最佳实践,以便在项目中保持一致的代码风格。学习并实践这些命名规范,将使你成为更出色的Java程序员。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java,Java命名规范, 变量命名, 方法命名, 类命名, 接口命名, 代码风格, 代码质量 + - Java --- +# Java命名规范(非常全面,可以收藏) + “二哥,Java 中的命名约定都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。 “对于我们中国人来说,名字也是有讲究的,比如说我叫沉默王二,你就叫沉默王三,哈哈。”我笑着对三妹说。 @@ -25,27 +19,32 @@ head: 拿我这个笔名“沉默王二”来举例吧,读起来我就觉得朗朗上口,读者看到这个笔名就知道我是一个什么样的人——对不熟的人保持沉默,对熟的人妙语连珠,哈哈。 ->当然了,如果你暂时记不住也没关系,后面再回头来记一下就好了。 - ### 01、包(package) 包的命名应该遵守以下规则: - 应该全部是小写字母 + - 点分隔符之间有且仅有一个自然语义的英语单词 + - 包名统一使用单数形式,比如说 `com.itwanger.util` 不能是 `com.itwanger.utils` + - 在最新的 Java 编程规范中,要求开发人员在自己定义的包名前加上唯一的前缀。由于互联网上的域名是不会重复的,所以多数开发人员采用自己公司(或者个人博客)在互联网上的域名称作为包的唯一前缀。比如我文章中出现的代码示例的包名就是 `package com.itwanger`。 + ### 02、类(class) 类的命名应该遵守以下规则: - 必须以大写字母开头 + - 最好是一个名词,比如说 System + - 类名使用 UpperCamelCase(驼峰式命名)风格 -- 尽量不要省略成单词的首字母,但以下情形例外:DO/BO/DTO/VO/AO/PO/UID 等 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/fifteen-01.png) +- 尽量不要省略成单词的首字母,但以下情形例外:DO/BO/DTO/VO/AO/ PO / UID 等 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/fifteen-01.png) 另外,如果是抽象类的话,使用 Abstract 或 Base 开头;如果是异常类的话,使用 Exception 结尾;如果是测试类的话,使用 Test 结尾。 @@ -54,7 +53,9 @@ head: 接口的命名应该遵守以下规则: - 必须以大写字母开头 + - 最好是一个形容词,比如说 Runnable + - 尽量不要省略成单词的首字母 来看个例子: @@ -66,6 +67,7 @@ interface Printable {} 接口和实现类之间也有一些规则: - 实现类用 Impl 的后缀与接口区别,比如说 CacheServiceImpl 实现 CacheService 接口 + - 或者,AbstractTranslator 实现 Translatable 接口 ### 04、字段(field)和变量(variable) @@ -73,10 +75,15 @@ interface Printable {} 字段和变量的命名应该遵守以下规则: - 必须以小写字母开头 + - 可以包含多个单词,第一个单词的首字母小写,其他的单词首字母大写,比如说 `firstName` + - 最好不要使用单个字符,比如说 `int a`,除非是局部变量 + - 类型与中括号紧挨相连来表示数组,比如说 `int[] arrayDemo`,main 方法中字符串数组参数不应该写成 `String args[]` + - POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误,我自己知道的有 fastjson + - 避免在子类和父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。子类、父类成员变量名相同,即使是 public 类型的变量也能够通过编译,另外,局部变量在同一方法内的不同代码块中同名也是合法的,这些情况都要避免。 反例: @@ -108,7 +115,9 @@ class Son extends ConfusingName { 常量的命名应该遵守以下规则: - 应该全部是大写字母 + - 可以包含多个单词,单词之间使用“_”连接,比如说 `MAX_PRIORITY`,力求语义表达完整清楚,不要嫌名字长 + - 可以包含数字,但不能以数字开头 来看个例子: @@ -117,12 +126,15 @@ class Son extends ConfusingName { static final int MIN_AGE = 18; ``` + ### 06、方法(method) 方法的命名应该遵守以下规则: - 必须以小写字母开头 + - 最好是一个动词,比如说 `print()` + - 可以包含多个单词,第一个单词的首字母小写,其他的单词首字母大写,比如说 `actionPerformed()` 来看个例子: @@ -134,10 +146,15 @@ void writeBook(){} Service/DAO 层的方法命名规约: - 获取单个对象的方法用 get 做前缀 + - 获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects + - 获取统计值的方法用 count 做前缀 + - 插入的方法用 save/insert 做前缀 + - 删除的方法用 remove/delete 做前缀 + - 修改的方法用 update 做前缀 @@ -157,9 +174,4 @@ Service/DAO 层的方法命名规约: ----- -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-extra-meal/java-unicode.md b/docs/basic-extra-meal/java-unicode.md new file mode 100644 index 0000000000..35ca351e2d --- /dev/null +++ b/docs/basic-extra-meal/java-unicode.md @@ -0,0 +1,175 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 彻底弄懂Java中的Unicode和UTF-8编码 + +“二哥,[上一篇](https://mp.weixin.qq.com/s/twim3w_dp5ctCigjLGIbFw)文章中提到了 Unicode,说 Java 中的 + char 类型之所以占 2 个字节,是因为 Java 使用的是 Unicode 字符集而不是 ASCII 字符集,我有点迷,想了解一下,能细致给我说说吗?” + +“当然没问题啊,三妹。” + +**1)ASCII** + +对于计算机来说,只认 0 和 1,所有的信息最终都是一个二进制数。一个二进制数要么是 0,要么是 1,所以 8 个二进制数放在一起(一个字节),就会组合出 256 种状态,也就是 2 的 8 次方(`2^8`),从 00000000 到 11111111。 + +ASCII 码由电报码发展而来,第一版标准发布于 1963 年,最后一次更新则是在1986 年,至今为止共定义了 128 个字符。其中 33 个字符无法显示在一般的设备上,需要用特殊的设备才能显示。 + +ASCII 码的局限在于只能显示 26 个基本拉丁字母、阿拉伯数字和英式标点符号,因此只能用于显示现代美国英语,对于其他一些语言则无能无力,比如在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-01.png) + +PS:拉丁字母(也称为罗马字母)是多数欧洲语言采用的字母系统,是世界上最通行的字母文字系统,是罗马文明的成果之一。 + +虽然名称上叫作拉丁字母,但拉丁文中并没有用 J、U 和 W 这三个字母。 + +在我们的印象中,可能说拉丁字母多少有些陌生,说英语字母可能就有直观的印象了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-02.png) + +PPS:阿拉伯数字,我们都很熟悉了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-03.png) + +但是,阿拉伯数字并非起源于阿拉伯,而是起源于古印度。学过历史的我们应该有一些印象,阿拉伯分布于西亚和北非,以阿拉伯语为主要语言,以伊斯兰教为主要信仰。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-04.png) + +处在这样的地理位置,做起东亚和欧洲的一些生意就很有优势,于是阿拉伯数字就由阿拉伯人传到了欧洲,因此得名。 + +PPPS:英式标点符号,也叫英文标点符号,和中文标点符号很相近。标点符号是辅助文字记录语言的符号,是书面语的组成部分,用来表示停顿、加强语气等。 + +英文标点符号在 16 世纪时,分为朗诵学派和句法学派,主要由古典时期的希腊文和拉丁文演变而来,在 17 世纪后进入稳定阶段。俄文的标点符号依据希腊文而来,到了 18 世纪后也采用了英文标点符号。 + +在很多人的印象中,古文是没有标点符号的,但管锡华博士研究指出,中国早在先秦时代就有标点符号了,后来融合了一些英文标点符号后,逐渐形成了现在的中文标点符号。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-05.png) + + +**2)Unicode** + +这个世界上,除了英语,还有法语、葡萄牙语、西班牙语、德语、俄语、阿拉伯语、韩语、日语等等等等。ASCII 码用来表示英语是绰绰有余的,但其他这些语言就没办法了。 + +像我们的母语,博大精深,汉字的数量很多很多,东汉的《说文解字》收字 9353 个,清朝《康熙字典》收字 47035 个,当代的《汉语大字典》收字 60370 个。1994 年中华书局、中国友谊出版公司出版的《中华字海》收字 85568 个。 + +PS:常用字大概 2500 个,次常用字 1000 个。 + +一个字节只能表示 256 种符号,所以如果拿 ASCII 码来表示汉字的话,是远远不够用的,那就必须要用更多的字节。简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,理论上最多可以表示 256 x 256 = 65536 个符号。 + +要知道,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-06.png) + +PPS:这“锟斤拷”价格挺公道的啊!!!(逃 + +如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会彻底消失。 + +这个艰巨的任务有谁来完成呢?**Unicode**,中文译作万国码、国际码、统一码、单一码,就像它的名字都表示的,这是一种所有符号的编码。 + +Unicode 至今仍在不断增修,每个新版本都会加入更多新的字符。目前最新的版本为 2020 年 3 月公布的 13.0,收录了 13 万个字符。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-07.png) + +Unicode 是一个很大的集合,现在的规模可以容纳 100 多万个符号。每个符号的编码都不一样,比如,`U+0639`表示阿拉伯字母 `Ain`,`U+0041` 表示英语的大写字母 `A`,`U+4E25` 表示汉字`严`。 + +具体的符号对应表,可以查询 [unicode.org](http://www.unicode.org/),或者专门的[汉字对应表](http://www.chi2ko.com/tool/CJK.htm)。 + +曾有人这样说: + +>Unicode 支持的字符上限是 65536 个,Unicode 字符必须占两个字节。 + +但这是一种误解,记住,Unicode 只是一个用来映射字符和数字的标准。它对支持字符的数量没有限制,也不要求字符必须占两个、三个或者其它任意数量的字节,所以它可以无穷大。 + +Unicode 虽然统一了全世界字符的编码,但没有规定如何存储。如果统一规定的话,每个符号就要用 3 个或 4 个字节表示,因为 2 个字节只能表示 65536 个,根本表示不全。 + +那怎么办呢? + +UTF(Unicode Transformation Formats,Unicode 的编码方式)来了!最常见的就是 UTF-8 和 UTF-16。 + +在 UTF-8 中,0-127 号的字符用 1 个字节来表示,使用和 ASCII 相同的编码。只有 128 号及以上的字符才用 2 个、3 个或者 4 个字节来表示。 + +如果只有一个字节,那么最高的比特位为 0;如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。 + +具体的表现形式为: + +0xxxxxxx:一个字节; +110xxxxx 10xxxxxx:两个字节编码形式(开始两个 1); +1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式(开始三个 1); +11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式(开始四个 1)。 + +也就是说,UTF-8 是一种可变长度的编码方式——这是它的优势也是劣势。 + +怎么讲呢?优势就是它包罗万象,劣势就是浪费空间。举例来说吧,UTF-8 采用了 3 个字节(256*256*256=16777216)来编码常用的汉字,但常用的汉字没有这么多,这对于计算机来说,就是一种严重的资源浪费。 + +基于这样的考虑,中国国家标准总局于 1980 年发布了 GB 2312 编码,即中华人民共和国国家标准简体中文字符集。GB 2312 标准共收录 6763 个汉字(2 个字节就够用了),其中一级汉字 3755 个,二级汉字 3008 个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的 682 个字符。 + +GB 2312 的出现,基本满足了汉字的计算机处理需求。对于人名、古汉语等方面出现的罕用字和繁体字,GB 2312 不能处理,就有了 GBK(K 为“扩展”的汉语拼音(kuòzhǎn)第一个声母)。 + +来看一段代码: + +```java +public class Demo { + public static void main(String[] args) { + String wanger = "沉默王二"; + byte[] bytes = wanger.getBytes(Charset.forName("GBK")); + String result = new String(bytes, Charset.forName("UTF-8")); + System.out.println(result); + } +} +``` + +先用 GBK 编码,再用 UTF-8 解码,程序会输出什么呢? + +``` +��Ĭ���� +``` + +嘿嘿,乱码来了!在 Unicode 中,� 是一个特殊的符号,它用来表示无法显示,它的十六进制是 `0xEF 0xBF 0xBD`。那么两个 �� 就是 `0xEF 0xBF 0xBD 0xEF 0xBF 0xBD`,如果用 GBK 进行解码的话,就是大名鼎鼎的“**锟斤拷**”。 + +可以通过代码来验证一下: + +```java +// 输出 efbfbdefbfbd +System.out.println(HexUtil.encodeHex("��", Charset.forName("UTF-8"))); +// 借助 hutool 转成二进制 +byte[] testBytes = HexUtil.decodeHex("efbfbdefbfbd"); +// 使用 GBK 解码 +String testResult = new String(testBytes, Charset.forName("GBK")); +// 输出锟斤拷 +System.out.println(testResult); +``` + +PPPS:hutool 的使用方法可以参照我的另外一篇[文章](https://mp.weixin.qq.com/s/hso-Hm5NuFStMu3m0iz_0w)。 + +所以,以后再见到**锟斤拷**,第一时间想到 UTF-8 和 GBK 的转换问题准没错。 + +UTF-16 使用 2 个或者 4 个字节来存储字符。 + +- 对于 Unicode 编号范围在 0 ~ FFFF 之间的字符,UTF-16 使用两个字节存储。 + +- 对于 Unicode 编号范围在 10000 ~ 10FFFF 之间的字符,UTF-16 使用四个字节存储,具体来说就是:将字符编号的所有比特位分成两部分,较高的一些比特位用一个值介于 D800~DBFF 之间的双字节存储,较低的一些比特位(剩下的比特位)用一个值介于 DC00~DFFF 之间的双字节存储。 + +**3)char** + +搞清楚了 Unicode 之后,再回头来看 char 为什么是两个字节的问题,就很容易搞明白了。 + +在 Unicode 的设计之初,人们认为两个字节足以对世界上各种语言的所有字符进行编码,在 1991 年发布的 Unicode 1.0 中,仅用了 65536 个代码值中不到一半的部分。 + +所以,Java 决定采用 16 位的 Unicode 字符集([诞生于 90 年代](https://mp.weixin.qq.com/s/Ctouw652iC0qtrmjen9aEw))。也就是说,当时的 char 类型可以表示任意一个 Unicode 字符。 + +但是,不可避免的事情发生了,Unicode 收录的字符越来越多,超过了 65536 个(2 个字节的最大表示范围)。超过的部分怎么办呢?只能用两个 char 来表示了。 + +这个 `𐐷` 字符很特殊,Unicode 编码是 `U+10437`,它就无法使用一个 char 来表示,当你尝试用 char 来表示时,它会被 IDEA 转成 UTF-16 十六进制字符代码 `\uD801\uDC37`(与此同时,编译器会提醒你最好把它声明成 String 类型)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/ten-08.png) + +也就是说,在 Java 中,char 会占用两个字节,超出 char 的承受范围('\u0000'(0)和 '\uffff'(65,535))的字符,都将无法表示。 + + + +“好了,三妹,关于 Unicode 就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md b/docs/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md new file mode 100644 index 0000000000..4c0c9fee8a --- /dev/null +++ b/docs/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md @@ -0,0 +1,103 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)? + +在知乎上看到 R 大的这篇回答,着实感觉需要分享给在座的各位 javaer 们,真心透彻。 + +>https://www.zhihu.com/question/52311366/answer/130090347 + +----- + +首先是先问是不是再问为什么系列。 + +在JDK8u的jdk项目下做个很粗略的搜索: + +``` +mymbp:/Users/me/workspace/jdk8u/jdk/src +$ egrep -nr "for \\(\\s?;\\s?;" . | wc -l + 369 +mymbp:/Users/me/workspace/jdk8u/jdk/src +$ egrep -nr "while \\(true" . | wc -l + 323 +``` + +并没有差多少。 + +其次,for (;;) 在Java中的来源。个人看法是喜欢用这种写法的人,追根溯源是受到C语言里的写法的影响。这些人不一定是自己以前写C习惯了这样写,而可能是间接受以前写C的老师、前辈的影响而习惯这样写的。 + +在C语言里,如果不include某些头文件或者自己声明的话,是没有内建的_Bool / bool类型,也没有TRUE / FALSE / true / false这些_Bool / bool类型值的字面量的。 + +所以,假定没有include那些头文件或者自己define出上述字面量,一个不把循环条件写在while (...)括号里的while语句,最常见的是这样: +``` +while (1) { + /* ... */ + } +``` + + …但不是所有人都喜欢看到那个魔数“1”的。 + + 而用for (;;)来表达不写循环条件(也就是循环体内不用break或goto就会是无限循环)则非常直观——这就是for语句本身的功能,而且不需要写任何魔数。所以这个写法就流传下来了。 + +顺带一提,在Java里我是倾向于写while (true)的,不过我也不介意别人在他们自己的项目里写for (;;)。 + +===================================== + +至于Java里while (true)与for (;;)哪个“效率更高”。这种规范没有规定的问题,答案都是“看实现”,毕竟实现只要保证语义符合规范就行了,而效率并不在规范管得着的范畴内。 + +以Oracle/Sun JDK8u / OpenJDK8u的实现来看,首先看javac对下面俩语句的编译结果: + +```java +public void foo() { + int i = 0; + while (true) { i++; } + } + +/* + public void foo(); + Code: + stack=1, locals=2, args_size=1 + 0: iconst_0 + 1: istore_1 + 2: iinc 1, 1 + 5: goto 2 +*/ +``` + + +与 + +```java +public void bar() { + int i = 0; + for (;;) { i++; } + }``` + +/* + public void bar(); + Code: + stack=1, locals=2, args_size=1 + 0: iconst_0 + 1: istore_1 + 2: iinc 1, 1 + 5: goto 2 +*/ +``` + +连javac这种几乎什么优化都不做(只做了Java语言规范规定一定要做的常量折叠,和非常少量别的优化)的编译器,对上面俩版本的代码都生成了一样的字节码。后面到解释执行、JIT编译之类的就不用说了,输入都一样,输出也不会不同。 + +----- + +分享的最后,二哥简单说几句。 + +可能在我们普通人眼中,这种问题完全没有求真的必要性,但 R大认真去研究了,并且得出了非常令人信服的答案。 + +所以,牛逼之人必有三连之处啊。 + +以后就可以放心大胆在代码里写 `for(;;) while(true)` 这样的死循环了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/jdk9-char-byte-string.md b/docs/basic-extra-meal/jdk9-char-byte-string.md new file mode 100644 index 0000000000..8d7c9d6df5 --- /dev/null +++ b/docs/basic-extra-meal/jdk9-char-byte-string.md @@ -0,0 +1,111 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# jdk9为何要将String的底层实现由char[]改成了byte[]? + +大家好,我是二哥呀!如果你不是 Java8 的钉子户,你应该早就发现了:String 类的源码已经由 `char[]` 优化为了 `byte[]` 来存储字符串内容,为什么要这样做呢? + +开门见山地说,从 `char[]` 到 `byte[]`,最主要的目的是**为了节省字符串占用的内存**。内存占用减少带来的另外一个好处,就是 GC 次数也会减少。 + +### 一、为什么要优化 String 节省内存空间 + +我们使用 `jmap -histo:live pid | head -n 10` 命令就可以查看到堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。 + +以我正在运行着的编程喵喵项目实例(基于 Java 8)来说,结果是这样的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/jdk9-char-byte-string-d826ce88-bbbe-47a3-a1a9-4dd86dd3632f.png) + +其中 String 对象有 17638 个,占用了 423312 个字节的内存,排在第三位。 + +由于 Java 8 的 String 内部实现仍然是 `char[]`,所以我们可以看到内存占用排在第 1 位的就是 char 数组。 + +`char[]` 对象有 17673 个,占用了 1621352 个字节的内存,排在第一位。 + +那也就是说优化 String 节省内存空间是非常有必要的,如果是去优化一个使用频率没有 String 这么高的类库,就显得非常的鸡肋。 + +### 二、`byte[]` 为什么就能节省内存空间呢? + +众所周知,char 类型的数据在 JVM 中是占用两个字节的,并且使用的是 UTF-8 编码,其值范围在 '\u0000'(0)和 '\uffff'(65,535)(包含)之间。 + + + +也就是说,使用 `char[]` 来表示 String 就导致了即使 String 中的字符只用一个字节就能表示,也得占用两个字节。 + +而实际开发中,单字节的字符使用频率仍然要高于双字节的。 + +当然了,仅仅将 `char[]` 优化为 `byte[]` 是不够的,还要配合 Latin-1 的编码方式,该编码方式是用单个字节来表示字符的,这样就比 UTF-8 编码节省了更多的空间。 + +换句话说,对于: + +```java +String name = "jack"; +``` + +这样的,使用 Latin-1 编码,占用 4 个字节就够了。 + +但对于: + +```java +String name = "小二"; +``` + +这种,木的办法,只能使用 UTF16 来编码。 + +针对 JDK 9 的 String 源码里,为了区别编码方式,追加了一个 coder 字段来区分。 + +```java +/** + * The identifier of the encoding used to encode the bytes in + * {@code value}. The supported values in this implementation are + * + * LATIN1 + * UTF16 + * + * @implNote This field is trusted by the VM, and is a subject to + * constant folding if String instance is constant. Overwriting this + * field after construction will cause problems. + */ +private final byte coder; +``` + +Java 会根据字符串的内容自动设置为相应的编码,要么 Latin-1 要么 UTF16。 + +也就是说,从 `char[]` 到 `byte[]`,**中文是两个字节,纯英文是一个字节,在此之前呢,中文是两个字节,应为也是两个字节**。 + +### 三、为什么用UTF-16而不用UTF-8呢? + +在 UTF-8 中,0-127 号的字符用 1 个字节来表示,使用和 ASCII 相同的编码。只有 128 号及以上的字符才用 2 个、3 个或者 4 个字节来表示。 + +- 如果只有一个字节,那么最高的比特位为 0; +- 如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。 + +具体的表现形式为: + +- 0xxxxxxx:一个字节; +- 110xxxxx 10xxxxxx:两个字节编码形式(开始两个 1); - 1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式(开始三个 1); +- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式(开始四个 1)。 + +关于字符编码,我在《Java 程序员进阶之路》里曾讲到过,想要深入了解的小伙伴查看下面的链接🔗: + +>https://tobebetterjavaer.com/basic-extra-meal/java-unicode.html + +也就是说,UTF-8 是变长的,那对于 String 这种有随机访问方法的类来说,就很不方便。所谓的随机访问,就是charAt、subString这种方法,随便指定一个数字,String要能给出结果。如果字符串中的每个字符占用的内存是不定长的,那么进行随机访问的时候,就需要从头开始数每个字符的长度,才能找到你想要的字符。 + +那有小伙伴可能会问,UTF-16也是变长的呢?一个字符还可能占用 4 个字节呢? + +的确,UTF-16 使用 2 个或者 4 个字节来存储字符。 + +- 对于 Unicode 编号范围在 0 ~ FFFF 之间的字符,UTF-16 使用两个字节存储。 +- 对于 Unicode 编号范围在 10000 ~ 10FFFF 之间的字符,UTF-16 使用四个字节存储,具体来说就是:将字符编号的所有比特位分成两部分,较高的一些比特位用一个值介于 D800~DBFF 之间的双字节存储,较低的一些比特位(剩下的比特位)用一个值介于 DC00~DFFF 之间的双字节存储。 + +但是在 Java 中,一个字符(char)就是 2 个字节,占 4 个字节的字符,在 Java 里也是用两个 char 来存储的,而String的各种操作,都是以Java的字符(char)为单位的,charAt是取得第几个char,subString取的也是第几个到第几个char组成的子串,甚至length返回的都是char的个数。 + +所以UTF-16在Java的世界里,就可以视为一个定长的编码。 + +>参考链接:https://www.zhihu.com/question/447224628 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/basic-extra-meal/override-overload.md b/docs/basic-extra-meal/override-overload.md new file mode 100644 index 0000000000..65d351ac58 --- /dev/null +++ b/docs/basic-extra-meal/override-overload.md @@ -0,0 +1,314 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java重写(Override)与重载(Overload) + +### 01、开篇 + +入冬的夜,总是来得特别的早。我静静地站在阳台,目光所及之处,不过是若隐若现的钢筋混凝土,还有那毫无情调的灯光。 + +“哥,别站在那发呆了。今天学啥啊,七点半我就要回学校了,留给你的时间不多了,你要抓紧哦。”三妹傲娇的声音一下子把我从游离的状态拉回到了现实。 + +“今天要学习 Java 中的方法重载与方法重写。”我迅速地走到电脑前面,打开一份 Excel 文档,看了一下《教妹学 Java》的进度,然后对三妹说。 + +“如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。 ”我面带着朴实无华的微笑继续说,“如果方法的功能是一样的,但参数不同,使用相同的名字可以提高程序的可读性。” + +“如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体可能不同),我们称之为方法重写。 方法重写用于提供父类已经声明的方法的特殊实现,是实现多态的基础条件。” + +“只不过,方法重载与方法重写在名字上很相似,就像是兄弟俩,导致初学者经常把它们俩搞混。” + +“方法重载的英文名叫 Overloading,方法重写的英文名叫 Overriding,因此,不仅中文名很相近,英文名之间也很相近,这就更容易让初学者搞混了。” + +“但两者其实是完全不同的!通过下面这张图,你就能看得一清二楚。” + +话音刚落,我就在 IDEA 中噼里啪啦地敲了起来。两段代码,分别是方法重写和方法重载。然后,把这两段代码截图到 draw.io(一个很漂亮的在线画图网站)上,加了一些文字说明。最后,打开 Photoscape X,把两张图片合并到了一起。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/21-01.png) + +### 02、方法重载 + +“三妹,你仔细听哦。”我缓了一口气后继续说道。 + +“在 Java 中,有两种方式可以达到方法重载的目的。” + +“第一,改变参数的数目。来看下面这段代码。” + +```java +public class OverloadingByParamNum { + public static void main(String[] args) { + System.out.println(Adder.add(10, 19)); + System.out.println(Adder.add(10, 19, 20)); + } +} + +class Adder { + static int add(int a, int b) { + return a + b; + } + + static int add(int a, int b, int c) { + return a + b + c; + } +} +``` + +“Adder 类有两个方法,第一个 `add()` 方法有两个参数,在调用的时候可以传递两个参数;第二个 `add()` 方法有三个参数,在调用的时候可以传递三个参数。” + +“二哥,这样的代码不会显得啰嗦吗?如果有四个参数的时候就再追加一个方法?”三妹突然提了一个很尖锐的问题。 + +“那倒是,这个例子只是为了说明方法重载的一种类型。如果参数类型相同的话,Java 提供了可变参数的方式,就像下面这样。” + +```java +static int add(int ... args) { + int sum = 0; + for ( int a: args) { + sum += a; + } + return sum; +} +``` + +“第二,通过改变参数类型,也可以达到方法重载的目的。来看下面这段代码。” + +```java +public class OverloadingByParamType { + public static void main(String[] args) { + System.out.println(Adder.add(10, 19)); + System.out.println(Adder.add(10.1, 19.2)); + } +} + +class Adder { + static int add(int a, int b) { + return a + b; + } + + static double add(double a, double b) { + return a + b; + } +} +``` + +“Adder 类有两个方法,第一个 `add()` 方法的参数类型为 int,第二个 `add()` 方法的参数类型为 double。” + +“二哥,改变参数的数目和类型都可以实现方法重载,为什么改变方法的返回值类型就不可以呢?”三妹很能抓住问题的重点嘛。 + +“因为仅仅改变返回值类型的话,会把编译器搞懵逼的。”我略带调皮的口吻回答她。 + +“编译时报错优于运行时报错,所以当两个方法的名字相同,参数个数和类型也相同的时候,虽然返回值类型不同,但依然会提示方法已经被定义的错误。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/21-02.png) + +“你想啊,三妹。我们在调用一个方法的时候,可以指定返回值类型,也可以不指定。当不指定的时候,直接指定 `add(1, 2)` 的时候,编译器就不知道该调用返回 int 的 `add()` 方法还是返回 double 的 `add()` 方法,产生了歧义。” + +“方法的返回值只是作为方法运行后的一个状态,它是保持方法的调用者和被调用者进行通信的一个纽带,但并不能作为某个方法的‘标识’。” + +“二哥,我想到了一个点,`main()` 方法可以重载吗?” + +“三妹,这是个好问题啊!答案是肯定的,毕竟 `main()` 方法也是个方法,只不过,Java 虚拟机在运行的时候只会调用带有 String 数组的那个 `main()` 方法。” + +```java +public class OverloadingMain { + public static void main(String[] args) { + System.out.println("String[] args"); + } + + public static void main(String args) { + System.out.println("String args"); + } + + public static void main() { + System.out.println("无参"); + } +} +``` + +“第一个 `main()` 方法的参数形式为 `String[] args`,是最标准的写法;第二个 `main()` 方法的参数形式为 `String args`,少了中括号;第三个 `main()` 方法没有参数。” + +“来看一下程序的输出结果。” + +``` +String[] args +``` + +“从结果中,我们可以看得出,尽管 `main()` 方法可以重载,但程序只认标准写法。” + +“由于可以通过改变参数类型的方式实现方法重载,那么当传递的参数没有找到匹配的方法时,就会发生隐式的类型转换。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/21-03.png) + +“如上图所示,byte 可以向上转换为 short、int、long、float 和 double,short 可以向上转换为 int、long、float 和 double,char 可以向上转换为 int、long、float 和 double,依次类推。” + +“三妹,来看下面这个示例。” + +```java +public class OverloadingTypePromotion { + void sum(int a, long b) { + System.out.println(a + b); + } + + void sum(int a, int b, int c) { + System.out.println(a + b + c); + } + + public static void main(String args[]) { + OverloadingTypePromotion obj = new OverloadingTypePromotion(); + obj.sum(20, 20); + obj.sum(20, 20, 20); + } +} +``` + +“执行 `obj.sum(20, 20)` 的时候,发现没有 `sum(int a, int b)` 的方法,所以此时第二个 20 向上转型为 long,所以调用的是 `sum(int a, long b)` 的方法。” + +“再来看一个示例。” + +```java +public class OverloadingTypePromotion1 { + void sum(int a, int b) { + System.out.println("int"); + } + + void sum(long a, long b) { + System.out.println("long"); + } + + public static void main(String args[]) { + OverloadingTypePromotion1 obj = new OverloadingTypePromotion1(); + obj.sum(20, 20); + } +} +``` + +“执行 `obj.sum(20, 20)` 的时候,发现有 `sum(int a, int b)` 的方法,所以就不会向上转型为 long,调用 `sum(long a, long b)`。” + +“来看一下程序的输出结果。” + +``` +int +``` + +“继续来看示例。” + +```java +public class OverloadingTypePromotion2 { + void sum(long a, int b) { + System.out.println("long int"); + } + + void sum(int a, long b) { + System.out.println("int long"); + } + + public static void main(String args[]) { + OverloadingTypePromotion2 obj = new OverloadingTypePromotion2(); + obj.sum(20, 20); + } +} +``` + +“二哥,我又想到一个问题。当有两个方法 `sum(long a, int b)` 和 `sum(int a, long b)`,参数个数相同,参数类型相同,只不过位置不同的时候,会发生什么呢?” + +“当通过 `obj.sum(20, 20)` 来调用 sum 方法的时候,编译器会提示错误。” +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/21-04.png) + +“不明确,编译器会很为难,究竟是把第一个 20 从 int 转成 long 呢,还是把第二个 20 从 int 转成 long,智障了!所以,不能写这样让编译器左右为难的代码。” + +### 03、方法重写 + +“三妹,累吗?我们稍微休息一下吧。”我把眼镜摘下来,放到桌子上,闭上了眼睛,开始胡思乱想起来。 + +2000 年,周杰伦横空出世,让青黄不接的唱片行业为之一振,由此开启了新一代天王争霸的黄金时代。2020 年,杰伦胖了,也贪玩了,一年出一张单曲都变得可遇不可求。 + +20 年前,程序员很稀有;20 年后,程序员内卷了。时间永远不会停下脚步,明年会不会好起来呢? + +“哥,醒醒,你就说休息一会,没说睡着啊。赶紧,我还有半个小时就要走了。” + +我戴上眼镜,对三妹继续说道:“在 Java 中,方法重写需要满足以下三个规则。” + +- 重写的方法必须和父类中的方法有着相同的名字; +- 重写的方法必须和父类中的方法有着相同的参数; +- 必须是 is-a 的关系(继承关系)。 + +“来看下面这段代码。” + +```java +public class Bike extends Vehicle { + public static void main(String[] args) { + Bike bike = new Bike(); + bike.run(); + } +} + +class Vehicle { + void run() { + System.out.println("车辆在跑"); + } +} +``` + +“来看一下程序的输出结果。” + +``` +车辆在跑 +``` + +“Bike is-a Vehicle,自行车是一种车,没错。Vehicle 类有一个 `run()` 的方法,也就是说车辆可以跑,Bike 继承了 Vehicle,也可以跑。但如果 Bike 没有重写 `run()` 方法的话,自行车就只能是‘车辆在跑’,而不是‘自行车在跑’,对吧?” + +“如果有了方法重写,一切就好办了。” + +```java +public class Bike extends Vehicle { + @Override + void run() { + System.out.println("自行车在跑"); + } + + public static void main(String[] args) { + Bike bike = new Bike(); + bike.run(); + } +} + +class Vehicle { + void run() { + System.out.println("车辆在跑"); + } +} +``` + +我把鼠标移动到 Bike 类的 `run()` 方法,对三妹说:“你看,在方法重写的时候,IDEA 会建议使用 `@Override` 注解,显式的表示这是一个重写后的方法,尽管可以缺省。” + +“来看一下程序的输出结果。” + +``` +自行车在跑 +``` + +“Bike 重写了 `run()` 方法,也就意味着,Bike 可以跑出自己的风格。” + +### 04、总结 + +“好了,三妹,我来简单做个总结。”我瞥了一眼电脑右上角的时钟,离三妹离开的时间不到 10 分钟了。 + +“首先来说一下方法重载时的注意事项,‘两同一不同’。” + +“‘两同’:在同一个类,方法名相同。” + +“‘一不同’:参数不同。” + +“再来说一下方法重写时的注意事项,‘两同一小一大’。” + +“‘两同’:方法名相同,参数相同。” + +“‘一小’:子类方法声明的异常类型要比父类小一些或者相等。” + +“‘一大’:子类方法的访问权限应该比父类的更大或者相等。” + +“记住了吧?三妹。带上口罩,拿好手机,咱准备出门吧。”今天限号,没法开车送三妹去学校了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-extra-meal/pass-by-value.md b/docs/basic-extra-meal/pass-by-value.md new file mode 100644 index 0000000000..a422174eff --- /dev/null +++ b/docs/basic-extra-meal/pass-by-value.md @@ -0,0 +1,142 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java到底是值传递还是引用传递? + +“哥,说说 Java 到底是值传递还是引用传递吧?”三妹一脸的困惑,看得出来她被这个问题折磨得不轻。 + +“说实在的,我在一开始学 Java 的时候也被这个问题折磨得够呛,总以为基本数据类型在传参的时候是值传递,而引用类型是引用传递。”我对三妹袒露了心声,为的就是让她不再那么焦虑,她哥当年也是这么过来的。 + + C 语言是很多编程语言的母胎,包括 Java,那么对于 C 语言来说,所有的方法参数都是“通过值”传递的,也就是说,传递给被调用方法的参数值存放在临时变量中,而不是存放在原来的变量中。这就意味着,被调用的方法不能修改调用方法中变量的值,而只能修改其私有变量的临时副本的值。 + +Java 继承了 C 语言这一特性,因此 Java 是按照值来传递的。 + +接下来,我们必须得搞清楚,到底什么是值传递(pass by value),什么是引用传递(pass by reference),否则,讨论 Java 到底是值传递还是引用传递就显得毫无意义。 + +当一个参数按照值的方式在两个方法之间传递时,调用者和被调用者其实是用的两个不同的变量——被调用者中的变量(原始值)是调用者中变量的一份拷贝,对它们当中的任何一个变量修改都不会影响到另外一个变量,据说 Fortran 语言是通过引用传递的。 + +“Fortran 语言?”三妹睁大了双眼,似乎听见了什么新的名词。 + +“是的,Fortran 语言,1957 年由 IBM 公司开发,是世界上第一个被正式采用并流传至今的高级编程语言。” + +当一个参数按照引用传递的方式在两个方法之间传递时,调用者和被调用者其实用的是同一个变量,当该变量被修改时,双方都是可见的。 + +“我们之所以容易搞不清楚 Java 到底是值传递还是引用传递,主要是因为 Java 中的两类数据类型的叫法容易引发误会,比如说 int 是基本类型,说它是值传递的,我们就很容易理解;但对于引用类型,比如说 String,说它也是值传递的时候,我们就容易弄不明白。” + +我们来看看基本数据类型和引用数据类型之间的差别。 + +```java +int age = 18; +String name = "二哥"; +``` + +age 是基本类型,值就保存在变量中,而 name 是引用类型,变量中保存的是对象的地址。一般称这种变量为对象的引用,引用存放在栈中,而对象存放在堆中。 + +这里说的栈和堆,是指内存中的一块区域,和数据结构中的栈和堆不一样。栈是由编译器自动分配释放的,所以适合存放编译期就确定生命周期的数据;而堆中存放的数据,编译器是不需要知道生命周期的,创建后的回收工作由垃圾收集器来完成。 + +“画幅图。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/pass-by-value-01.png) + +当用 = 赋值运算符改变 age 和 name 的值时。 + +```java +age = 16; +name = "三妹"; +``` + +对于基本类型 age,赋值运算符会直接改变变量的值,原来的值被覆盖。 + +对于引用类型 name,赋值运算符会改变对象引用中保存的地址,原来的地址被覆盖,但原来的对象不会被覆盖。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/pass-by-value-02.png) + +“三妹,注意听,接下来,我们来说说基本数据类型的参数传递。” + +Java 有 8 种基本数据类型,分别是 int、long、byte、short、float、double 、char 和 boolean,就拿 int 类型来举例吧。 + +```java +class PrimitiveTypeDemo { + public static void main(String[] args) { + int age = 18; + modify(age); + System.out.println(age); + } + + private static void modify(int age1) { + age1 = 30; + } +} +``` + +1)`main()` 方法中的 age 为基本类型,所以它的值 18 直接存储在变量中。 + +2)调用 `modify()` 方法的时候,将会把 age 的值 18 复制给形参 age1。 + +3)`modify()` 方法中,对 age1 做出了修改。 + +4)回到 `main()` 方法中,age 的值仍然为 18,并没有发生改变。 + +如果我们想让 age 的值发生改变,就需要这样做。 + +```java +class PrimitiveTypeDemo1 { + public static void main(String[] args) { + int age = 18; + age = modify(age); + System.out.println(age); + } + + private static int modify(int age1) { + age1 = 30; + return age1; + } +} +``` + +第一,让 `modify()` 方法有返回值; + +第二,使用赋值运算符重新对 age 进行赋值。 + +“好了,再来说说引用类型的参数传递。” + +就以 String 为例吧。 + +```java +class ReferenceTypeDemo { + public static void main(String[] args) { + String name = "二哥"; + modify(name); + System.out.println(name); + } + + private static void modify(String name1) { + name1 = "三妹"; + } +} +``` + +在调用 `modify()` 方法的时候,形参 name1 复制了 name 的地址,指向的是堆中“二哥”的位置。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/pass-by-value-03.png) + +当 `modify()` 方法调用结束后,改变了形参 name1 的地址,但 `main()` 方法中 name 并没有发生改变。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/pass-by-value-04.png) + +总结: + +- Java 中的参数传递是按值传递的。 +- 如果参数是基本类型,传递的是基本类型的字面量值的拷贝。 +- 如果参数是引用类型,传递的是引用的对象在堆中地址的拷贝。 + +“好了,三妹,今天的学习就到这吧。” + + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/src/basic-extra-meal/true-generic.md b/docs/basic-extra-meal/true-generic.md similarity index 78% rename from docs/src/basic-extra-meal/true-generic.md rename to docs/basic-extra-meal/true-generic.md index 0b3356dd3a..50a877ba0f 100644 --- a/docs/src/basic-extra-meal/true-generic.md +++ b/docs/basic-extra-meal/true-generic.md @@ -1,22 +1,15 @@ --- -title: Java 为什么无法实现真正的泛型? -shortTitle: Java为什么无法实现真正的泛型 category: - Java核心 tag: - - Java重要知识点 -description: Java 无法实现真正泛型的原因在于类型擦除,这种设计是为了兼容早期 Java 版本。本文详细探讨 Java 泛型的实现机制、类型擦除背后的原理,以及 Java 泛型在编程中的局限性。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java, 泛型, 类型擦除 + - Java --- +# Java不能实现真正泛型的原因是什么? -“二哥,为啥 Java 不能实现真正的泛型啊?”三妹开门见山地问。 +“二哥,为啥 Java 不能实现真正泛型啊?”三妹开门见山地问。 -简单来回顾一下[类型擦除](https://javabetter.cn/basic-extra-meal/generic.html),看下面这段代码。 +简单来回顾一下类型擦除,看下面这段代码。 ```java public class Cmower { @@ -58,7 +51,8 @@ public class Cmower { “保持耐心,好不好?”我安慰道。 -**第一,兼容性** + +第一,兼容性 Java 在 2004 年已经积累了较为丰富的生态,如果把现有的类修改为泛型类,需要让所有的用户重新修改源代码并且编译,这就会导致 Java 1.4 之前打下的江山可能会完全覆灭。 @@ -68,9 +62,9 @@ Java 在 2004 年已经积累了较为丰富的生态,如果把现有的类修 老用户不受影响,新用户可以自由地选择使用泛型,可谓一举两得。 -**第二,不是“实现不了”**。Pizza,1996 年的实验语言,在 Java 的基础上扩展了泛型。 +第二,不是“实现不了”。Pizza,1996 年的实验语言,在 Java 的基础上扩展了泛型。 ->Pizza 教程地址:[http://pizzacompiler.sourceforge.net/doc/tutorial.html](http://pizzacompiler.sourceforge.net/doc/tutorial.html) +>Pizza 教程地址:http://pizzacompiler.sourceforge.net/doc/tutorial.html “1996 年?”三妹表示很吃惊。 @@ -120,7 +114,7 @@ String s = a.get(); 对吧?这就是我们想要的“真正意义上的泛型”,A 不仅仅可以是引用类型 String,还可以是基本数据类型。要知道,Java 的泛型不允许是基本数据类型,只能是包装器类型。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/true-generic-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/generic/true-generic-01.png) 除此之外,Pizza 的泛型还可以直接使用 `new` 关键字进行声明,并且 Pizza 编译器会从构造方法的参数上推断出具体的对象类型,究竟是 String 还是 int。要知道,Java 的泛型因为类型擦除的原因,程序员是无法知道一个 ArrayList 究竟是 `ArrayList` 还是 `ArrayList` 的。 @@ -187,11 +181,11 @@ Java 语言和其他编程语言不一样,有着沉重的历史包袱,1.5 Java 一直以来都强调兼容性,我认为这也是 Java 之所以能被广泛使用的主要原因之一,开发者不必担心 Java 版本升级的问题,一个在 JDK 1.4 上可以跑的代码,放在 JDK 1.5 上仍然可以跑。 -这里必须得说明一点,J2SE1.5 的发布,是 Java 语言发展史上的重要里程碑,为了表示该版本的重要性,J2SE1.5 也正式更名为 Java SE 5.0,往后去就是 Java SE 6.0,Java SE 7.0。。。。 +*这里必须得说明一点,J2SE1.5 的发布,是 Java 语言发展史上的重要里程碑,为了表示该版本的重要性,J2SE1.5 也正式更名为 Java SE 5.0,往后去就是 Java SE 6.0,Java SE 7.0。。。。* 但 Java 并不支持高版本 JDK 编译生成的字节码文件在低版本的 JRE(Java 运行时环境)上跑。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/true-generic-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/generic/true-generic-02.png) 针对泛型,兼容性具体表现在什么地方呢?来看下面这段代码。 @@ -220,12 +214,12 @@ Java 神奇就神奇在这,表面上万物皆对象,但为了性能上的考 一个好消息是 Valhalla 项目正在努力解决这些因为泛型擦除带来的历史遗留问题。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/true-generic-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/generic/true-generic-03.png) Project Valhalla:正在进行当中的 OpenJDK 项目,计划给未来的 Java 添加改进的泛型支持。 ->源码地址:[http://openjdk.java.net/projects/valhalla/](http://openjdk.java.net/projects/valhalla/) +>源码地址:http://openjdk.java.net/projects/valhalla/ 让我们拭目以待吧! @@ -233,11 +227,4 @@ Project Valhalla:正在进行当中的 OpenJDK 项目,计划给未来的 Jav “嗯嗯。二哥,你讲得可真棒👍”三妹夸奖得我有点小开心,嘿嘿。 ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-extra-meal/varables.md b/docs/basic-extra-meal/varables.md new file mode 100644 index 0000000000..ea9b0a8ec7 --- /dev/null +++ b/docs/basic-extra-meal/varables.md @@ -0,0 +1,144 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java中可变参数的使用 + +为了让铁粉们能白票到阿里云的服务器,老王当了整整两天的客服,真正体验到了什么叫做“为人民群众谋福利”的不易和辛酸。正在他眼睛红肿打算要休息之际,小二跑过来问他:“Java 的可变参数究竟是怎么一回事?”老王一下子又清醒了,他爱 Java,他爱传道解惑,他爱这群尊敬他的读者。 + +可变参数是 Java 1.5 的时候引入的功能,它允许方法使用任意多个、类型相同(`is-a`)的值作为参数。就像下面这样。 + +```java +public static void main(String[] args) { + print("沉"); + print("沉", "默"); + print("沉", "默", "王"); + print("沉", "默", "王", "二"); +} + +public static void print(String... strs) { + for (String s : strs) + System.out.print(s); + System.out.println(); +} +``` + +静态方法 `print()` 就使用了可变参数,所以 `print("沉")` 可以,`print("沉", "默")` 也可以,甚至 3 个、 4 个或者更多个字符串都可以作为参数传递给 `print()` 方法。 + +说到可变参数,我想起来阿里巴巴开发手册上有这样一条规约。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/varables-01.png) + +意思就是尽量不要使用可变参数,如果要用的话,可变参数必须要在参数列表的最后一位。既然坑位有限,只能在最后,那么可变参数就只能有一个(悠着点,悠着点)。如果可变参数不在最后一位,IDE 就会提示对应的错误,如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/varables-02.png) + + + + +可变参数看起来就像是个语法糖,它背后究竟隐藏了什么呢?老王想要一探究竟,它在追求真理这条路上一直很执着。 + +其实也很简单。**当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法**。 + +这就是为什么可以使用数组作为参数来调用带有可变参数的方法的根本原因。代码如下所示。 + +```java +public static void main(String[] args) { + print(new String[]{"沉"}); + print(new String[]{"沉", "默"}); + print(new String[]{"沉", "默", "王"}); + print(new String[]{"沉", "默", "王", "二"}); +} + +public static void print(String... strs) { + for (String s : strs) + System.out.print(s); + System.out.println(); +} +``` + +那如果方法的参数是一个数组,然后像使用可变参数那样去调用方法的时候,能行得通吗? + +*留个思考题,大家也可以去试一试* + + + +那一般什么时候使用可变参数呢? + +可变参数,可变参数,顾名思义,当一个方法需要处理任意多个相同类型的对象时,就可以定义可变参数。Java 中有一个很好的例子,就是 String 类的 `format()` 方法,就像下面这样。 + +```java +System.out.println(String.format("年纪是: %d", 18)); +System.out.println(String.format("年纪是: %d 名字是: %s", 18, "沉默王二")); +``` + +`%d` 表示将整数格式化为 10 进制整数,`%s` 表示输出字符串。 + +如果不使用可变参数,那需要格式化的参数就必须使用“+”号操作符拼接起来了。麻烦也就惹上身了。 + +在实际的项目代码中,开源包 slf4j.jar 的日志输出就经常要用到可变参数(log4j 就没法使用可变参数,日志中需要记录多个参数时就痛苦不堪了)。就像下面这样。 + +```java +protected Logger logger = LoggerFactory.getLogger(getClass()); +logger.debug("名字是{}", mem.getName()); +logger.debug("名字是{},年纪是{}", mem.getName(), mem.getAge()); +``` + +查看源码就可以发现,`debug()` 方法使用了可变参数。 + +```java +public void debug(String format, Object... arguments); +``` + +那在使用可变参数的时候有什么注意事项吗? + +有的。我们要避免重载带有可变参数的方法——这样很容易让编译器陷入自我怀疑中。 + +```java +public static void main(String[] args) { + print(null); +} + +public static void print(String... strs) { + for (String a : strs) + System.out.print(a); + System.out.println(); +} + +public static void print(Integer... ints) { + for (Integer i : ints) + System.out.print(i); + System.out.println(); +} +``` + +这时候,编译器完全不知道该调用哪个 `print()` 方法,`print(String... strs)` 还是 `print(Integer... ints)`,傻傻分不清。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/varables-03.png) + + +假如真的需要重载带有可变参数的方法,就必须在调用方法的时候给出明确的指示,不要让编译器去猜。 + +```java +public static void main(String[] args) { + String [] strs = null; + print(strs); + + Integer [] ints = null; + print(ints); +} + +public static void print(String... strs) { +} + +public static void print(Integer... ints) { +} +``` + +上面这段代码是可以编译通过的。因为编译器知道参数是 String 类型还是 Integer 类型,只不过为了运行时不抛出 `NullPointerException`,两个 `print()` 方法的内部要做好判空操作。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-grammar/basic-data-type.md b/docs/basic-grammar/basic-data-type.md new file mode 100644 index 0000000000..74d41d15e6 --- /dev/null +++ b/docs/basic-grammar/basic-data-type.md @@ -0,0 +1,330 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java 支持的 8 种基本数据类型 + +“二哥,[上一节](https://mp.weixin.qq.com/s/IgBpLGn0L1HZymgI4hWGVA)提到了 Java 变量的数据类型,是不是指定了类型就限定了变量的取值范围啊?”三妹吸了一口麦香可可奶茶后对我说。 + +“三妹,你不得了啊,长进很大嘛,都学会推理判断了。Java 是一种静态类型的编程语言,这意味着所有变量必须在使用之前声明好,也就是必须得先指定变量的类型和名称。” + +Java 中的数据类型可分为 2 种: + +1)**基本数据类型**。 + +基本数据类型是 Java 语言操作数据的基础,包括 boolean、char、byte、short、int、long、float 和 double,共 8 种。 + +2)**引用数据类型**。 + +除了基本数据类型以外的类型,都是所谓的引用类型。常见的有数组(对,没错,数组是引用类型)、class(也就是类),以及接口(指向的是实现接口的类的对象)。 + +来个思维导图,感受下。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-01.png) + +通过[上一节](https://mp.weixin.qq.com/s/IgBpLGn0L1HZymgI4hWGVA)的学习,我们知道变量可以分为局部变量、成员变量、静态变量。 + +当变量是局部变量的时候,必须得先初始化,否则编译器不允许你使用它。拿 int 来举例吧,看下图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-02.png) + +当变量是成员变量或者静态变量时,可以不进行初始化,它们会有一个默认值,仍然以 int 为例,来看代码: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class LocalVar { + private int a; + static int b; + + public static void main(String[] args) { + LocalVar lv = new LocalVar(); + System.out.println(lv.a); + System.out.println(b); + } +} +``` + +来看输出结果: + +``` +0 +0 +``` + +瞧见没,int 作为成员变量时或者静态变量时的默认值是 0。那不同的基本数据类型,是有不同的默认值和大小的,来个表格感受下。 + +| 数据类型 | 默认值 | 大小 | +| -------- | -------- | ----- | +| boolean | false | 1比特 | +| char | '\u0000' | 2字节 | +| byte | 0 | 1字节 | +| short | 0 | 2字节 | +| int | 0 | 4字节 | +| long | 0L | 8字节 | +| float | 0.0f | 4字节 | +| double | 0.0 | 8字节 | + +那三妹可能要问,“比特和字节是什么鬼?” + +比特币听说过吧?字节跳动听说过吧?这些名字当然不是乱起的,确实和比特、字节有关系。 + +**1)bit(比特)** + +比特作为信息技术的最基本存储单位,非常小,但大名鼎鼎的比特币就是以此命名的,它的简写为小写字母“b”。 + +大家都知道,计算机是以二进制存储数据的,二进制的一位,就是 1 比特,也就是说,比特要么为 0 要么为 1。 + +**2)Byte(字节)** + +通常来说,一个英文字符是一个字节,一个中文字符是两个字节。字节与比特的换算关系是:1 字节 = 8 比特。 + +在往上的单位就是 KB,并不是 1000 字节,因为计算机只认识二进制,因此是 2 的 10 次方,也就是 1024 个字节。 + +(终于知道 1024 和程序员的关系了吧?狗头保命) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-03.png) + +接下来,我们再来详细地了解一下 8 种基本数据类型。 + +### 01、布尔 + +布尔(boolean)仅用于存储两个值:true 和 false,也就是真和假,通常用于条件的判断。代码示例: + +```java +boolean flag = true; +``` + + +### 02、byte + +byte 的取值范围在 -128 和 127 之间,包含 127。最小值为 -128,最大值为 127,默认值为 0。 + +在网络传输的过程中,为了节省空间,常用字节来作为数据的传输方式。代码示例: + + +```java +byte a = 10; +byte b = -10; +``` + + + + +### 03、short + +short 的取值范围在 -32,768 和 32,767 之间,包含 32,767。最小值为 -32,768,最大值为 32,767,默认值为 0。代码示例: + +```java +short s = 10000; +short r = -5000; +``` + + + +### 04、int + +int 的取值范围在 -2,147,483,648(-2 ^ 31)和 2,147,483,647(2 ^ 31 -1)(含)之间,默认值为 0。如果没有特殊需求,整型数据就用 int。代码示例: + +```java +int a = 100000; +int b = -200000; +``` + +### 05、long + +long 的取值范围在 -9,223,372,036,854,775,808(-2^63) 和 9,223,372,036,854,775,807(2^63 -1)(含)之间,默认值为 0。如果 int 存储不下,就用 long,整型数据就用 int。代码示例: + +```java +long a = 100000L; +long b = -200000L; +``` + +为了和 int 作区分,long 型变量在声明的时候,末尾要带上大写的“L”。不用小写的“l”,是因为小写的“l”容易和数字“1”混淆。 + +### 06、float + +float 是单精度的浮点数,遵循 IEEE 754(二进制浮点数算术标准),取值范围是无限的,默认值为 0.0f。float 不适合用于精确的数值,比如说货币。代码示例: + +```java +float f1 = 234.5f; +``` + +为了和 double 作区分,float 型变量在声明的时候,末尾要带上小写的“f”。不需要使用大写的“F”,是因为小写的“f”很容易辨别。 + + +### 07、double + +double 是双精度的浮点数,遵循 IEEE 754(二进制浮点数算术标准),取值范围也是无限的,默认值为 0.0。double 同样不适合用于精确的数值,比如说货币。代码示例: + +```java +double d1 = 12.3 +``` + +那精确的数值用什么表示呢?最好使用 BigDecimal,它可以表示一个任意大小且精度完全准确的浮点数。针对货币类型的数值,也可以先乘以 100 转成整型进行处理。 + +Tips:单精度是这样的格式,1 位符号,8 位指数,23 位小数,有效位数为 7 位。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-04.png) + +双精度是这样的格式,1 位符号,11 位指数,52 为小数,有效位数为 16 位。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-05.png) + +取值范围取决于指数位,计算精度取决于小数位(尾数)。小数位越多,则能表示的数越大,那么计算精度则越高。 + +>一个数由若干位数字组成,其中影响测量精度的数字称作有效数字,也称有效数位。有效数字指科学计算中用以表示一个浮点数精度的那些数字。一般地,指一个用小数形式表示的浮点数中,从第一个非零的数字算起的所有数字。如 1.24 和 0.00124 的有效数字都有 3 位。 + +### 08、char + +char 可以表示一个 16 位的 Unicode 字符,其值范围在 '\u0000'(0)和 '\uffff'(65,535)(包含)之间。代码示例: + +```java +char letterA = 'A'; // 用英文的单引号包裹住。 +``` + +那三妹可能要问,“char 既然只有一个字符,为什么占 2 个字节呢?” + +“主要是因为 Java 使用的是 Unicode 字符集而不是 ASCII 字符集。” + +这又是为什么呢?我们留到下一节再讲。 + +基本数据类型在作为成员变量和静态变量的时候有默认值,引用数据类型也有的。 + +String 是最典型的引用数据类型,所以我们就拿 String 类举例,看下面这段代码: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class LocalRef { + private String a; + static String b; + + public static void main(String[] args) { + LocalRef lv = new LocalRef(); + System.out.println(lv.a); + System.out.println(b); + } +} +``` + +输出结果如下所示: + +``` +null +null +``` + +null 在 Java 中是一个很神奇的存在,在你以后的程序生涯中,见它的次数不会少,尤其是伴随着令人烦恼的“[空指针异常](https://mp.weixin.qq.com/s/PBqR_uj6dd4xKEX8SUWIYQ)”,也就是所谓的 `NullPointerException`。 + +也就是说,引用数据类型的默认值为 null,包括数组和接口。 + +那三妹是不是很好奇,为什么数组和接口也是引用数据类型啊? + +先来看数组: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 java + */ +public class ArrayDemo { + public static void main(String[] args) { + int [] arrays = {1,2,3}; + System.out.println(arrays); + } +} +``` + +arrays 是一个 int 类型的数组,对吧?打印结果如下所示: + +``` +[I@2d209079 +``` + +`[I` 表示数组是 int 类型的,@ 后面是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 `java.lang.Object` 类的 `toString()` 方法就明白了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-06.png) + +数组虽然没有显式定义成一个类,但它的确是一个对象,继承了祖先类 Object 的所有方法。那为什么数组不单独定义一个类来表示呢?就像字符串 String 类那样呢? + +一个合理的解释是 Java 将其隐藏了。假如真的存在一个 Array.java,我们也可以假想它真实的样子,它必须要定义一个容器来存放数组的元素,就像 String 类那样。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; +} +``` + +数组内部定义数组?没必要的! + +再来看接口: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class IntefaceDemo { + public static void main(String[] args) { + List list = new ArrayList<>(); + System.out.println(list); + } +} +``` + +List 是一个非常典型的接口: + +```java +public interface List extends Collection {} +``` + +而 ArrayList 是 List 接口的一个实现: + +```java +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable +{} +``` + +对于接口类型的引用变量来说,你没法直接 new 一个: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/nine-07.png) + + +只能 new 一个实现它的类的对象——那自然接口也是引用数据类型了。 + +来看一下基本数据类型和引用数据类型之间最大的差别。 + +基本数据类型: + +- 1、变量名指向具体的数值。 +- 2、基本数据类型存储在栈上。 + +引用数据类型: + +- 1、变量名指向的是存储对象的内存地址,在栈上。 +- 2、内存地址指向的对象存储在堆上。 + +看到这,三妹是不是又要问,“堆是什么,栈又是什么?” + +堆是堆(heap),栈是栈(stack),如果看到“堆栈”的话,请不要怀疑自己,那是翻译的错,堆栈也是栈,反正我很不喜欢“堆栈”这种叫法,容易让新人掉坑里。 + +堆是在程序运行时在内存中申请的空间(可理解为动态的过程);切记,不是在编译时;因此,Java 中的对象就放在这里,这样做的好处就是: + +>当需要一个对象时,只需要通过 new 关键字写一行代码即可,当执行这行代码时,会自动在内存的“堆”区分配空间——这样就很灵活。 + +栈,能够和处理器(CPU,也就是脑子)直接关联,因此访问速度更快。既然访问速度快,要好好利用啊!Java 就把对象的引用放在栈里。为什么呢?因为引用的使用频率高吗? + +不是的,因为 Java 在编译程序时,必须明确的知道存储在栈里的东西的生命周期,否则就没法释放旧的内存来开辟新的内存空间存放引用——空间就那么大,前浪要把后浪拍死在沙滩上啊。 + +这么说就理解了吧? + +“好了,三妹,关于 Java 中的数据类型就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-grammar/flow-control.md b/docs/basic-grammar/flow-control.md new file mode 100644 index 0000000000..2a9a10f486 --- /dev/null +++ b/docs/basic-grammar/flow-control.md @@ -0,0 +1,909 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java流程控制语句 + +“二哥,流程控制语句都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。 + +“比如说 if-else、switch、for、while、do-while、return、break、continue 等等,接下来,我们一个个来了解下。” + +### 01、if-else 相关 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-01.png) + + +**1)if 语句** + +if 语句的格式如下: + +```java +if(布尔表达式){ +// 如果条件为 true,则执行这块代码 +} +``` + +画个流程图表示一下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-02.png) + + +来写个示例: + +```java +public class IfExample { + public static void main(String[] args) { + int age = 20; + if (age < 30) { + System.out.println("青春年华"); + } + } +} +``` + +输出: + +``` +青春年华 +``` + +**2)if-else 语句** + +if-else 语句的格式如下: + +```java +if(布尔表达式){ +// 条件为 true 时执行的代码块 +}else{ +// 条件为 false 时执行的代码块 +} +``` + +画个流程图表示一下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-03.png) + + +来写个示例: + +```java +public class IfElseExample { + public static void main(String[] args) { + int age = 31; + if (age < 30) { + System.out.println("青春年华"); + } else { + System.out.println("而立之年"); + } + } +} +``` + +输出: + +``` +而立之年 +``` + +除了这个例子之外,还有一个判断闰年(被 4 整除但不能被 100 整除或者被 400 整除)的例子: + +```java +public class LeapYear { + public static void main(String[] args) { + int year = 2020; + if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) { + System.out.println("闰年"); + } else { + System.out.println("普通年份"); + } + } +} +``` + +输出: + +``` +闰年 +``` + +如果执行语句比较简单的话,可以使用三元运算符来代替 if-else 语句,如果条件为 true,返回 ? 后面 : 前面的值;如果条件为 false,返回 : 后面的值。 + +```java +public class IfElseTernaryExample { + public static void main(String[] args) { + int num = 13; + String result = (num % 2 == 0) ? "偶数" : "奇数"; + System.out.println(result); + } +} +``` + +输出: + +``` +奇数 +``` + +**3)if-else-if 语句** + +if-else-if 语句的格式如下: + +```java +if(条件1){ +// 条件1 为 true 时执行的代码 +}else if(条件2){ +// 条件2 为 true 时执行的代码 +} +else if(条件3){ +// 条件3 为 true 时执行的代码 +} +... +else{ +// 以上条件均为 false 时执行的代码 +} +``` + +画个流程图表示一下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-04.png) + + +来写个示例: + +```java +public class IfElseIfExample { + public static void main(String[] args) { + int age = 31; + if (age < 30) { + System.out.println("青春年华"); + } else if (age >= 30 && age < 40 ) { + System.out.println("而立之年"); + } else if (age >= 40 && age < 50 ) { + System.out.println("不惑之年"); + } else { + System.out.println("知天命"); + } + } +} +``` + +输出: + +``` +而立之年 +``` + +**4)if 嵌套语句** + +if 嵌套语句的格式如下: + +```java +if(外侧条件){ + // 外侧条件为 true 时执行的代码 + if(内侧条件){ + // 内侧条件为 true 时执行的代码 + } +} +``` + +画个流程图表示一下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-05.png) + + +来写个示例: + +```java +public class NestedIfExample { + public static void main(String[] args) { + int age = 20; + boolean isGirl = true; + if (age >= 20) { + if (isGirl) { + System.out.println("女生法定结婚年龄"); + } + } + } +} +``` + +输出: + +``` +女生法定结婚年龄 +``` + +### 02、switch 语句 + +switch 语句用来判断变量与多个值之间的相等性。变量的类型可以是 byte、short、int、long,或者对应的包装器类型 Byte、Short、Integer、Long,以及字符串和枚举。 + +来看一下 switch 语句的格式: + +```java +switch(变量) { +case 可选值1: + // 可选值1匹配后执行的代码; + break; // 该关键字是可选项 +case 可选值2: + // 可选值2匹配后执行的代码; + break; // 该关键字是可选项 +...... + +default: // 该关键字是可选项 + // 所有可选值都不匹配后执行的代码 +} +``` + +- 变量可以有 1 个或者 N 个值。 + +- 值类型必须和变量类型是一致的,并且值是确定的。 + +- 值必须是唯一的,不能重复,否则编译会出错。 + +- break 关键字是可选的,如果没有,则执行下一个 case,如果有,则跳出 switch 语句。 + +- default 关键字也是可选的。 + + + +画个流程图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-06.png) + + + +来个示例: + +```java +public class Switch1 { + public static void main(String[] args) { + int age = 20; + switch (age) { + case 20 : + System.out.println("上学"); + break; + case 24 : + System.out.println("苏州工作"); + break; + case 30 : + System.out.println("洛阳工作"); + break; + default: + System.out.println("未知"); + break; // 可省略 + } + } +} +``` + +输出: + +``` +上学 +``` + +当两个值要执行的代码相同时,可以把要执行的代码写在下一个 case 语句中,而上一个 case 语句中什么也没有,来看一下示例: + +```java +public class Switch2 { + public static void main(String[] args) { + String name = "沉默王二"; + switch (name) { + case "詹姆斯": + System.out.println("篮球运动员"); + break; + case "穆里尼奥": + System.out.println("足球教练"); + break; + case "沉默王二": + case "沉默王三": + System.out.println("乒乓球爱好者"); + break; + default: + throw new IllegalArgumentException( + "名字没有匹配项"); + + } + } +} +``` + +输出: + +``` +乒乓球爱好者 +``` + +枚举作为 switch 语句的变量也很常见,来看例子: + +```java +public class SwitchEnumDemo { + public enum PlayerTypes { + TENNIS, + FOOTBALL, + BASKETBALL, + UNKNOWN + } + + public static void main(String[] args) { + System.out.println(createPlayer(PlayerTypes.BASKETBALL)); + } + + private static String createPlayer(PlayerTypes playerType) { + switch (playerType) { + case TENNIS: + return "网球运动员费德勒"; + case FOOTBALL: + return "足球运动员C罗"; + case BASKETBALL: + return "篮球运动员詹姆斯"; + case UNKNOWN: + throw new IllegalArgumentException("未知"); + default: + throw new IllegalArgumentException( + "运动员类型: " + playerType); + + } + } +} +``` + +输出: + +``` +篮球运动员詹姆斯 +``` + +### 03、for 循环 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-07.png) + +**1)普通 for 循环** + +普通的 for 循环可以分为 4 个部分: + +1)初始变量:循环开始执行时的初始条件。 + +2)条件:循环每次执行时要判断的条件,如果为 true,就执行循环体;如果为 false,就跳出循环。当然了,条件是可选的,如果没有条件,则会一直循环。 + +3)循环体:循环每次要执行的代码块,直到条件变为 false。 + +4)自增/自减:初识变量变化的方式。 + + + +来看一下普通 for 循环的格式: + + + +```java +for(初识变量;条件;自增/自减){ +// 循环体 +} +``` + + + +画个流程图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-08.png) + + + + +来个示例: + +```java +public class ForExample { + public static void main(String[] args) { + for (int i = 0; i < 5; i++) { + System.out.println("沉默王三好美啊"); + } + } +} +``` + +输出: + +``` +沉默王三好美啊 +沉默王三好美啊 +沉默王三好美啊 +沉默王三好美啊 +沉默王三好美啊 +``` + +“哎呀,二哥,你真的是变着法夸我啊。” + +“非也非也,三妹,你看不出我其实在夸我自己吗?循环语句还可以嵌套呢,这样就可以打印出更好玩的呢,你要不要看看?” + +“好呀好呀!” + +“看好了啊。” + +```java +public class PyramidForExample { + public static void main(String[] args) { + for (int i = 0; i < 5; i++) { + for (int j = 0;j<= i;j++) { + System.out.print("❤"); + } + System.out.println(); + } + } +} +``` + +打印出什么玩意呢? + +``` +❤ +❤❤ +❤❤❤ +❤❤❤❤ +❤❤❤❤❤ +``` + +“哇,太不可思议了,二哥。” + +“嘿嘿。” + +**2)for-each** + +for-each 循环通常用于遍历数组和集合,它的使用规则比普通的 for 循环还要简单,不需要初始变量,不需要条件,不需要下标来自增或者自减。来看一下语法: + +```java +for(元素类型 元素 : 数组或集合){ +// 要执行的代码 +} +``` + + +来看一下示例: + +```java +public class ForEachExample { + public static void main(String[] args) { + String[] strs = {"沉默王二", "一枚有趣的程序员"}; + + for (String str : strs) { + System.out.println(str); + } + } +} +``` + +输出: + +``` +沉默王二 +一枚有趣的程序员 +``` + +“呀,二哥,你开始王哥卖瓜了啊。” + +“嘿嘿,三妹,你这样说哥会脸红的。” + +**3)无限 for 循环** + +“三妹,你想不想体验一下无限 for 循环的威力,也就是死循环。” + +“二哥,那会有什么样的后果啊?” + +“来,看看就知道了。” + +```java +public class InfinitiveForExample { + public static void main(String[] args) { + for(;;){ + System.out.println("停不下来。。。。"); + } + } +} +``` + +输出: + +``` +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +``` + +一旦运行起来,就停不下来了,除非强制停止。 + +### 04、while 循环 + +来看一下 while 循环的格式: + + + +```java +while(条件){ +//循环体 +} +``` + + + +画个流程图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-09.png) + + + + + +来个示例: + +```java +public class WhileExample { + public static void main(String[] args) { + int i = 0; + while (true) { + System.out.println("沉默王三"); + i++; + if (i == 5) { + break; + } + } + } +} +``` + +“三妹,你猜猜会输出几次?” + +“五次吗?” + +“对了,你可真聪明。” + +``` +沉默王三 +沉默王三 +沉默王三 +沉默王三 +沉默王三 +``` + + + +“三妹,你想不想体验一下无限 while 循环的威力,也就是死循环。” + +“二哥,那会有什么样的后果啊?” + +“来,看看就知道了。” + +```java +public class InfinitiveWhileExample { + public static void main(String[] args) { + while (true) { + System.out.println("停不下来。。。。"); + } + } +} +``` + +输出: + +``` +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +``` + +把 while 的条件设置为 true,并且循环体中没有 break 关键字的话,程序一旦运行起来,就根本停不下来了,除非强制停止。 + +### 05、do-while 循环 + +来看一下 do-while 循环的格式: + + + +```java +do{ +// 循环体 +}while(提交); +``` + + + +画个流程图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-10.png) + + + + + + +来个示例: + +```java +public class DoWhileExample { + public static void main(String[] args) { + int i = 0; + do { + System.out.println("沉默王三"); + i++; + if (i == 5) { + break; + } + } while (true); + } +} +``` + +“三妹,你猜猜会输出几次?” + +“五次吗?” + +“对了,你可真聪明。” + +``` +沉默王三 +沉默王三 +沉默王三 +沉默王三 +沉默王三 +``` + + + +“三妹,你想不想体验一下无限 do-while 循环的威力......” + +“二哥,又来啊,我都腻了。” + +“来吧,例行公事,就假装看看嘛。” + +```java +public class InfinitiveDoWhileExample { + public static void main(String[] args) { + do { + System.out.println("停不下来。。。。"); + } while (true); + } +} +``` + +输出: + +``` +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +``` + +把 do-while 的条件设置为 true,并且循环体中没有 break 关键字的话,程序一旦运行起来,就根本停不下来了,除非强制停止。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-11.png) + +### 06、break + +break 关键字通常用于中断循环或 switch 语句,它在指定条件下中断程序的当前流程。如果是内部循环,则仅中断内部循环。 + +可以将 break 关键字用于所有类型循环语句中,比如说 for 循环、while 循环,以及 do-while 循环。 + +来画个流程图感受一下: + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/control/thirteen-12.png) + + +用在 for 循环中的示例: + +```java +for (int i = 1; i <= 10; i++) { + if (i == 5) { + break; + } + System.out.println(i); +} +``` + +用在嵌套 for 循环中的示例: + +```java +for (int i = 1; i <= 3; i++) { + for (int j = 1; j <= 3; j++) { + if (i == 2 && j == 2) { + break; + } + System.out.println(i + " " + j); + } +} +``` + +用在 while 循环中的示例: + +```java +int i = 1; +while (i <= 10) { + if (i == 5) { + i++; + break; + } + System.out.println(i); + i++; +} +``` + +用在 do-while 循环中的示例: + +```java +int j = 1; +do { + if (j == 5) { + j++; + break; + } + System.out.println(j); + j++; +} while (j <= 10); +``` + +用在 switch 语句中的示例: + +```java +switch (age) { + case 20 : + System.out.println("上学"); + break; + case 24 : + System.out.println("苏州工作"); + break; + case 30 : + System.out.println("洛阳工作"); + break; + default: + System.out.println("未知"); + break; // 可省略 +} +``` + +### 07、continue + +当我们需要在 for 循环或者 (do)while 循环中立即跳转到下一个循环时,就可以使用 continue 关键字,通常用于跳过指定条件下的循环体,如果循环是嵌套的,仅跳过当前循环。 + +来个示例: + +```java +public class ContinueDemo { + public static void main(String[] args) { + for (int i = 1; i <= 10; i++) { + if (i == 5) { + // 使用 continue 关键字 + continue;// 5 将会被跳过 + } + System.out.println(i); + } + } +} +``` + +输出: + +``` +1 +2 +3 +4 +6 +7 +8 +9 +10 +``` + +“二哥,5 真的被跳过了呀。” + +“那必须滴。不然就是 bug。” + +再来个循环嵌套的例子。 + +```java +public class ContinueInnerDemo { + public static void main(String[] args) { + for (int i = 1; i <= 3; i++) { + for (int j = 1; j <= 3; j++) { + if (i == 2 && j == 2) { + // 当i=2,j=2时跳过 + continue; + } + System.out.println(i + " " + j); + } + } + } +} +``` + +打印出什么玩意呢? + +``` +1 1 +1 2 +1 3 +2 1 +2 3 +3 1 +3 2 +3 3 +``` + +“2 2” 没有输出,被跳过了。 + +再来看一下 while 循环时 continue 的使用示例: + +```java +public class ContinueWhileDemo { + public static void main(String[] args) { + int i = 1; + while (i <= 10) { + if (i == 5) { + i++; + continue; + } + System.out.println(i); + i++; + } + } +} +``` + +输出: + +``` +1 +2 +3 +4 +6 +7 +8 +9 +10 +``` + +注意:如果把 if 条件中的“i++”省略掉的话,程序就会进入死循环,一直在 continue。 + +最后,再来看一下 do-while 循环时 continue 的使用示例: + +```java +public class ContinueDoWhileDemo { + public static void main(String[] args) { + int i=1; + do{ + if(i==5){ + i++; + continue; + } + System.out.println(i); + i++; + }while(i<=10); + } +} + +``` + +输出: + +``` +1 +2 +3 +4 +6 +7 +8 +9 +10 +``` + +注意:同样的,如果把 if 条件中的“i++”省略掉的话,程序就会进入死循环,一直在 continue。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-grammar/javadoc.md b/docs/basic-grammar/javadoc.md new file mode 100644 index 0000000000..f6107e34a5 --- /dev/null +++ b/docs/basic-grammar/javadoc.md @@ -0,0 +1,179 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java注释:单行、多行和文档注释 + +“二哥,Java 中的注释好像真没什么可讲的,我已经提前预习了,不过是单行注释,多行注释,还有文档注释。”三妹的脸上泛着甜甜的笑容,她竟然提前预习了接下来要学习的知识,有一种“士别三日,当刮目相看”的感觉。 + +“注释的种类确实不多,但还是挺有意思的,且听哥来给你说道说道。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-01.png) + + + + +### 01、单行注释 + +单行注释通常用于解释方法内某单行代码的作用。 + +```java +public void method() { + int age = 18; // age 用于表示年龄 +} +``` + +**但如果写在行尾的话,其实是不符合阿里巴巴的开发规约的**。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-02.png) + +正确的单行注释如上图中所说,在被注释语句上方另起一行,使用 `//` 注释。 + +```java +public void method() { + // age 用于表示年龄 + int age = 18; +} +``` + + +### 02、多行注释 + +多行注释使用的频率其实并不高,通常用于解释一段代码的作用。 + +```java +/* +age 用于表示年纪 +name 用于表示姓名 +*/ +int age = 18; +String name = "沉默王二"; +``` + +以 `/*` 开始,以 `*/` 结束,但不如用多个 `//` 来得痛快,因为 `*` 和 `/` 不在一起,敲起来麻烦。 + +```java +// age 用于表示年纪 +// name 用于表示姓名 +int age = 18; +String name = "沉默王二"; +``` + +### 03、文档注释 + +文档注释可用在三个地方,类、字段和方法,用来解释它们是干嘛的。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class Demo { + /** + * 姓名 + */ + private int age; + + /** + * main 方法作为程序的入口 + * + * @param args 参数 + */ + public static void main(String[] args) { + + } +} +``` + +PS:在 Intellij IDEA 中,按下 `/**` 后敲下回车键就可以自动添加文档注释的格式,`*/` 是自动补全的。 + +接下来,我们来看看如何通过 javadoc 命令生成代码文档。 + +**第一步**,在该类文件上右键,找到「Open in Terminal」 可以打开命令行窗口。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-03.png) + + +**第二步**,执行 javadoc 命令 `javadoc Demo.java -encoding utf-8`。`-encoding utf-8` 可以保证中文不发生乱码。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-04.png) + +**第三步,**执行 `ls -l` 命令就可以看到生成代码文档时产生的文件,主要是一些可以组成网页的 html、js 和 css 文件。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-05.png) + +**第四步**,执行 `open index.html` 命令可以通过默认的浏览器打开文档注释。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-06.png) + +点击「Demo」,可以查看到该类更具体的注释文档。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-07.png) + +### 04、文档注释的注意事项 + +1)`javadoc` 命令只能为 public 和 protected 修饰的字段、方法和类生成文档。 + +default 和 private 修饰的字段和方法的注释将会被忽略掉。因为我们本来就不希望这些字段和方法暴露给调用者。 + +如果类不是 public 的话,javadoc 会执行失败。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-08.png) + +2)文档注释中可以嵌入一些 HTML 标记,比如说段落标记 `

`,超链接标记 `` 等等,但不要使用标题标记,比如说 `

`,因为 javadoc 会插入自己的标题,容易发生冲突。 + +3)文档注释中可以插入一些 `@` 注解,比如说 `@see` 引用其他类,`@version` 版本号,`@param` 参数标识符,`@author` 作者标识符,`@deprecated` 已废弃标识符,等等。 + +### 05、注释规约 + +1)类、字段、方法必须使用文档注释,不能使用单行注释和多行注释。因为注释文档在 IDE 编辑窗口中可以悬浮提示,提高编码效率。 + +比如说,在使用 String 类的时候,鼠标悬停在 String 上时可以得到以下提示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-09.png) + +2)所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。 + +3)所有的类都必须添加创建者和创建日期。 + +Intellij IDEA 中可以在「File and Code Templates」中设置。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/fourteen-10.png) + +语法如下所示: + +``` +/** +* 微信搜索「沉默王二」,回复 Java +* @author 沉默王二 +* @date ${DATE} +*/ +``` + +设置好后,在新建一个类的时候就可以自动生成了。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/16 + */ +public class Test { +} +``` + +4)所有的枚举类型字段必须要有注释,说明每个数据项的用途。 + +5)代码修改的同时,注释也要进行相应的修改。 + + +“好了,三妹,关于 Java 中的注释就先说这么多吧。”转动了一下僵硬的脖子后,我对三妹说。“记住一点,注释是程序固有的一部分。” + +>第一、注释要能够准确反映设计思想和代码逻辑;第二、注释要能够描述业务含 义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同 天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看 的,使其能够快速接替自己的工作。 + +----- + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/basic-grammar/operator.md b/docs/basic-grammar/operator.md new file mode 100644 index 0000000000..e38544faf7 --- /dev/null +++ b/docs/basic-grammar/operator.md @@ -0,0 +1,379 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java运算符 + +“二哥,让我盲猜一下哈,运算符是不是指的就是加减乘除啊?”三妹的脸上泛着甜甜的笑容,我想她一定对提出的问题很有自信。 + +“是的,三妹。运算符在 Java 中占据着重要的位置,对程序的执行有着很大的帮助。除了常见的加减乘除,还有许多其他类型的运算符,来看下面这张思维导图。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/eleven-01.png) + + +### 01、算数运算符 + +算术运算符除了最常见的加减乘除,还有一个取余的运算符,用于得到除法运算后的余数,来串代码感受下。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class ArithmeticOperator { + public static void main(String[] args) { + int a = 10; + int b = 5; + + System.out.println(a + b);//15 + System.out.println(a - b);//5 + System.out.println(a * b);//50 + System.out.println(a / b);//2 + System.out.println(a % b);//0 + + b = 3; + System.out.println(a + b);//13 + System.out.println(a - b);//7 + System.out.println(a * b);//30 + System.out.println(a / b);//3 + System.out.println(a % b);//1 + } +} +``` + +对于初学者来说,加法(+)、减法(-)、乘法(*)很好理解,但除法(/)和取余(%)会有一点点疑惑。在以往的认知里,10/3 是除不尽的,结果应该是 3.333333...,而不应该是 3。相应的,余数也不应该是 1。这是为什么呢? + +因为数字在程序中可以分为两种,一种是整型,一种是浮点型(不清楚的同学可以回头看看[数据类型那篇](https://mp.weixin.qq.com/s/twim3w_dp5ctCigjLGIbFw)),整型和整型的运算结果就是整型,不会出现浮点型。否则,就会出现浮点型。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class ArithmeticOperator { + public static void main(String[] args) { + int a = 10; + float c = 3.0f; + double d = 3.0; + System.out.println(a / c); // 3.3333333 + System.out.println(a / d); // 3.3333333333333335 + System.out.println(a % c); // 1.0 + System.out.println(a % d); // 1.0 + } +} +``` + +需要注意的是,当浮点数除以 0 的时候,结果为 Infinity 或者 NaN。 + +``` +System.out.println(10.0 / 0.0); // Infinity +System.out.println(0.0 / 0.0); // NaN +``` + +Infinity 的中文意思是无穷大,NaN 的中文意思是这不是一个数字(Not a Number)。 + + +当整数除以 0 的时候(`10 / 0`),会抛出异常: + +``` +Exception in thread "main" java.lang.ArithmeticException: / by zero + at com.itwanger.eleven.ArithmeticOperator.main(ArithmeticOperator.java:32) +``` + +所以整数在进行除法运算时,需要先判断除数是否为 0,以免程序抛出异常。 + +算术运算符中还有两种特殊的运算符,自增运算符(++)和自减运算符(--),它们也叫做一元运算符,只有一个操作数。 + +```java +public class UnaryOperator1 { + public static void main(String[] args) { + int x = 10; + System.out.println(x++);//10 (11) + System.out.println(++x);//12 + System.out.println(x--);//12 (11) + System.out.println(--x);//10 + } +} +``` + +一元运算符可以放在数字的前面或者后面,放在前面叫前自增(前自减),放在后面叫后自增(后自减)。 + +前自增和后自增是有区别的,拿 `int y = ++x` 这个表达式来说(x = 10),它可以拆分为 `x = x+1 = 11; y = x = 11`,所以表达式的结果为 `x = 11, y = 11`。拿 `int y = x++` 这个表达式来说(x = 10),它可以拆分为 `y = x = 10; x = x+1 = 11`,所以表达式的结果为 `x = 11, y = 10`。 + +```java +int x = 10; +int y = ++x; +System.out.println(y + " " + x);// 11 11 + +x = 10; +y = x++; +System.out.println(y + " " + x);// 10 11 +``` + +对于前自减和后自减来说,同学们可以自己试一把。 + + +### 02、关系运算符 + +关系运算符用来比较两个操作数,返回结果为 true 或者 false。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/eleven-02.png) + +来看示例: + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class RelationOperator { + public static void main(String[] args) { + int a = 10, b = 20; + System.out.println(a == b); // false + System.out.println(a != b); // true + System.out.println(a > b); // false + System.out.println(a < b); // true + System.out.println(a >= b); // false + System.out.println(a <= b); // true + } +} +``` + +### 03、位运算符 + +在学习位运算符之前,需要先学习一下二进制,因为位运算符操作的不是整型数值(int、long、short、char、byte)本身,而是整型数值对应的二进制。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class BitOperator { + public static void main(String[] args) { + System.out.println(Integer.toBinaryString(60)); // 111100 + System.out.println(Integer.toBinaryString(13)); // 1101 + } +} +``` + + 从程序的输出结果可以看得出来,60 的二进制是 0011 1100(用 0 补到 8 位),13 的二进制是 0000 1101。 + +PS:现代的二进制记数系统由戈特弗里德·威廉·莱布尼茨于 1679 年设计。莱布尼茨是德意志哲学家、数学家,历史上少见的通才。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-grammar/eleven-03.png) + +来看示例: + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class BitOperator { + public static void main(String[] args) { + int a = 60, b = 13; + System.out.println("a 的二进制:" + Integer.toBinaryString(a)); // 111100 + System.out.println("b 的二进制:" + Integer.toBinaryString(b)); // 1101 + + int c = a & b; + System.out.println("a & b:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a | b; + System.out.println("a | b:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a ^ b; + System.out.println("a ^ b:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = ~a; + System.out.println("~a:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a << 2; + System.out.println("a << 2:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a >> 2; + System.out.println("a >> 2:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a >>> 2; + System.out.println("a >>> 2:" + c + ",二进制是:" + Integer.toBinaryString(c)); + } +} +``` + +对于初学者来说,位运算符无法从直观上去计算出结果,不像加减乘除那样。因为我们日常接触的都是十进制,位运算的时候需要先转成二进制,然后再计算出结果。 + +鉴于此,初学者在写代码的时候其实很少会用到位运算。对于编程高手来说,为了提高程序的性能,会在一些地方使用位运算。比如说,HashMap 在计算哈希值的时候: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +如果对位运算一点都不懂的话,遇到这样的源码就很吃力。所以说,虽然位运算用的少,但还是要懂。 + +1)按位左移运算符: + +```java +public class LeftShiftOperator { + public static void main(String[] args) { + System.out.println(10<<2);//10*2^2=10*4=40 + System.out.println(10<<3);//10*2^3=10*8=80 + System.out.println(20<<2);//20*2^2=20*4=80 + System.out.println(15<<4);//15*2^4=15*16=240 + } +} +``` + +`10<<2` 等于 10 乘以 2 的 2 次方;`10<<3` 等于 10 乘以 2 的 3 次方。 + +2)按位右移运算符: + +```java +public class RightShiftOperator { + public static void main(String[] args) { + System.out.println(10>>2);//10/2^2=10/4=2 + System.out.println(20>>2);//20/2^2=20/4=5 + System.out.println(20>>3);//20/2^3=20/8=2 + } +} +``` + +`10>>2` 等于 10 除以 2 的 2 次方;`20>>2` 等于 20 除以 2 的 2 次方。 + +### 04、逻辑运算符 + +逻辑与运算符(&&):多个条件中只要有一个为 false 结果就为 false。 + +逻辑或运算符(||):多个条件只要有一个为 true 结果就为 true。 + +```java +public class LogicalOperator { + public static void main(String[] args) { + int a=10; + int b=5; + int c=20; + System.out.println(ab||ab|a作者:朱晋君,转载链接:[https://mp.weixin.qq.com/s/xlPZfpd89rDq6L-Me80wnw](https://mp.weixin.qq.com/s/xlPZfpd89rDq6L-Me80wnw) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/chengdu.md b/docs/cityselect/chengdu.md similarity index 98% rename from docs/src/cityselect/chengdu.md rename to docs/cityselect/chengdu.md index e1bfef642a..f97c53fa8f 100644 --- a/docs/src/cityselect/chengdu.md +++ b/docs/cityselect/chengdu.md @@ -1,12 +1,11 @@ --- -shortTitle: 成都 category: - 求职面试 tag: - 城市选择 --- -# 成都都有哪些值得加入的IT互联网公司? +# 成都有哪些牛批的互联网公司? 今天应小伙伴们的要求,再来聊一聊美丽的**四川省成都市**有哪些酷酷的IT/技术类公司(不仅仅局限于纯互联网)。 @@ -484,4 +483,4 @@ tag: >链接:[https://mp.weixin.qq.com/s/eaX9QhLwy_VsGIH0apA4qw](https://mp.weixin.qq.com/s/eaX9QhLwy_VsGIH0apA4qw) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/cityselect/guangzhou.md b/docs/cityselect/guangzhou.md similarity index 77% rename from docs/src/cityselect/guangzhou.md rename to docs/cityselect/guangzhou.md index bbaedaa35a..f97c4e00e1 100644 --- a/docs/src/cityselect/guangzhou.md +++ b/docs/cityselect/guangzhou.md @@ -1,12 +1,11 @@ --- -shortTitle: 广州 category: - 求职面试 tag: - 城市选择 --- -# 广州都有哪些值得加入的IT互联网公司? +# 想去广州了! @@ -16,7 +15,7 @@ tag: 广州的繁华以及它的魅力不用我多说,无论从人文还是从经济来说都是不虚其它城市的。不过我在面试时发现,广州除了几个头部大厂是统一薪资标准外,相较于其它一线城市,广州的互联网行业给我的感觉是整体薪资水平偏低。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-0f0eb358-f7ae-46b9-b070-8b93e401735e.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-0f0eb358-f7ae-46b9-b070-8b93e401735e.png) 大家注意呀,其实学计算机相关专业的想在广州挣钱,不止可以通过互联网公司,也可以通过当公务员,教师等方式去挣钱呀。应届生考进体制内当公务员,年薪也20多万呢,并且各方面福利待遇绝对到位,相当不错了。下面我们还是分互联网公司以及国企央企研究所介绍吧。 @@ -28,61 +27,61 @@ tag: **网易** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-53893155-f8cb-4bfe-b0b4-768c43c03862.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-53893155-f8cb-4bfe-b0b4-768c43c03862.png) 网易在广州主要是游戏部门。算是网易的核心业务了。注意,网易面试时候是散装的,网易互娱、网易雷火和网易互联网是分开招聘的,我当时投简历都是分开给他们投的,同样的简历我在网站上填了三次,累死。不过好处就是你有三次机会能进网易 ~ 注意,互娱、雷火、互联网的薪资方案也是不一样的。网易的薪资方案很复杂,这里面水太深,我感觉我有点把握不住~ 我知道有人拿到sp大概就是 25k * 16吧,还会有股票。网易食堂的伙食是真的好,大部分人去了工作一段时间都胖了。 **微信** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-f4e50c0c-5214-484a-b5e9-18a20a929745.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-f4e50c0c-5214-484a-b5e9-18a20a929745.png) 广州主要是腾讯的 wxg 事业群,也就是微信。wxg 事业群的效益非常好,能进 wxg 还是很棒的,不过面试难度也挺大的。腾讯去年校招的薪资分 17k,18.5k,20k,21.5k,23k 几个档次,hr说是应届生第一年保证18薪,不过第二年就不保证了哈。注意,腾讯开出的薪资是可以argue的,如果他给你的薪资不满意,你可以用其它offer跟腾讯的 hr argue 更高档次的薪资,有成功的。另外校招生还有签字费以及股票以及租房补贴。腾讯时不时送点小礼物,工作的舒适度还是不错的。 **字节跳动** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-a1c6a805-e7fe-450f-bd18-c24d760b60e7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-a1c6a805-e7fe-450f-bd18-c24d760b60e7.png) 广州的字节整体上不如其它地方的字节加班严重,不过也要看部门,有的部门听说挺累的。字节之前一直是大小周,前不久刚刚宣布要取消大小周,有双休还是挺幸福的,就是要少挣不少钱了,看你是不是奋斗逼了。我看到21届字节校招的薪资主要是每月20k,22k,24k,26k,28k,30k不等。不过我看校招拿到广州这边offer的大部分是22k和24k的月薪, **欢聚时代** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-7d47dd3b-924e-4af3-99e9-85c77dca3d45.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-7d47dd3b-924e-4af3-99e9-85c77dca3d45.png) 大家看到欢聚时代这个名称可能有点陌生,其实 YY 和 虎牙 都是欢聚时代的。欢聚时代的薪资水平我不清楚,我问了朋友也不清楚,我就去查校招薪水上的薪资爆料。有点迷呀,去年校招,开发方向的月薪14k、16k、18k 的都有,但是这算法薪资的爆料咋还直接就 27k 呢?这也差的太多了吧。有靠谱消息的老铁欢迎来补充。 **唯品会** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-bd501c3a-05ac-4a1e-a27c-7645dc6d8ab0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-bd501c3a-05ac-4a1e-a27c-7645dc6d8ab0.png) 唯品会算是广州本地的一霸了吧,唯品会的福利挺给力的,包三餐,双休,租房补贴。本科生开发的月薪主要集中在14k-16k。 **SHEIN** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-d805d3a7-a5b8-4e5a-a304-e59cac38baad.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-d805d3a7-a5b8-4e5a-a304-e59cac38baad.png) 这家公司我熟,我就给你们展开讲讲。这家公司最近势头很猛,做跨境电商的,主要卖女装。我在去年秋招初期还面试过这家公司,面试难度比较友好。首先是 hr 给我发了个笔试链接,让我几天内找个时间做了。通过笔试后,第一轮面试,面试了大概有 30 多分钟,主要问了项目以及面试八股文,没有让写代码,然后面试就通过了。第二轮面试,面试官随便问了两个八股文问题就开始聊理想了。第三轮面试好像是他们的 cto,问我高考数学考多少分 ~ 问我上学期间大概写了多少行代码~ 然后又聊了几句就跟我结束面试了。就给我 offer了。后续hr跟我谈薪的时候说他们加班少,工资是 16k * 14,我觉得工资有点少,就没接offer。 **三七互娱** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-b7f04bcf-684c-4c89-acb2-5dc8907284a8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-b7f04bcf-684c-4c89-acb2-5dc8907284a8.png) 我了解到三七互娱后端岗位大部分是 PHP,本科生校招进来薪资大概是12k 左右吧,每年发 15个月的工资。在知乎上对三七互娱的讨论比较多,有黑点,每个部门的情况也不太一样,大家可以再去了解下呀。 **小鹏汽车** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-fbcfd7c0-79ad-48c5-ad67-471ff91666a5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-fbcfd7c0-79ad-48c5-ad67-471ff91666a5.png) 小鹏汽车最近风头正盛,应届生校招进来的工资基本在 16k 左右,每年15薪。小鹏汽车目前是大小周呀,工作强度还是比较大的。 **酷狗音乐** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-4e3962cb-109e-4f2d-a372-ba325444d415.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-4e3962cb-109e-4f2d-a372-ba325444d415.png) 酷狗音乐是腾讯的这应该大家都知道,不过内部的薪资职级不是完全对齐的哈。酷狗的不同部门工作强度差别很大,有的闲的要死,有的忙的要死。选择部门时要注意。 **4399** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-6a8653f4-b840-4f46-af16-72fa00b9019b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-6a8653f4-b840-4f46-af16-72fa00b9019b.png) 4399大家应该都不陌生,好多小游戏都是从小开始玩的,工资也在16k左右。 @@ -96,19 +95,19 @@ tag: **移动** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-66cca36c-656b-4e6e-9172-f68d79e78af9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-66cca36c-656b-4e6e-9172-f68d79e78af9.png) 移动每月的工资很低,可能到手就五六千块钱,然后年底会一下再发五六万的年终奖,有时候会更多一些。据说 18 年效益好年底一下发了 8 万,19 年就拉跨了。 **联通** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-cc0cdb0f-034c-4a63-81f6-a17beed70b14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-cc0cdb0f-034c-4a63-81f6-a17beed70b14.png) 转正后每月到手八九千,绩效好能到手一万左右。 **电信** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-ff7fd7ae-3bf8-4797-bf07-f9490da269b9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-ff7fd7ae-3bf8-4797-bf07-f9490da269b9.png) 我看到 offershow 上有做网络运维的爆料,入职第一年每月到手8.5k,年终发了3.5万。 @@ -128,13 +127,13 @@ tag: ### 房价 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-4b8f3cec-ca0d-4b39-9826-755cd2fe29b6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-4b8f3cec-ca0d-4b39-9826-755cd2fe29b6.png) 大家直接看图,相比于其它一线城市房价算是便宜了。但是按照广州互联网的工资水平想在广州差不多的区域和地段买房,压力是很大的。 ### 教育 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-75763e2a-3b9b-48e4-a5dc-b0308d60e98f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-75763e2a-3b9b-48e4-a5dc-b0308d60e98f.png) 广州的985大学有两所,分别是中山大学和华南理工,211也有两所是暨南大学以及华南师范大学,另外广州大学、广东工业大学等等也都不错。广州的高中也很给力,华南师范大学附属中学、广东实验中学、广东广雅中学、广州市执信中学、广州二中、六中等等学校都很不错。 @@ -142,17 +141,17 @@ tag: 一线城市的医疗资源肯定是没问题的,我粗略数了一下,广州有不下40所三甲医院。我了解到顶级医疗资源主要有中山系、南方系、广中医系等等。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-5b5c6c29-cc79-4c0b-be57-b105babd0141.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-5b5c6c29-cc79-4c0b-be57-b105babd0141.png) ### 风景&美食 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-4dbf092b-e820-4114-8fd8-ab7e0ac59f8d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-4dbf092b-e820-4114-8fd8-ab7e0ac59f8d.png) 广州的美景自然不用多说,另外空气质量也很好,说起空气质量,我这个北方人已经习惯冬天经常见不到太阳了。广州的美食也很多,白切鸡、煲仔饭和肠粉等等,还有很多我就不报菜名了,我在西安上学时食堂有个卖肠粉的窗口,我很爱吃,不过这个肠粉明显是本地化了。话说南北差异还是挺大的,我一个同学去广州上学,去洗澡带着搓澡巾,他的广州舍友都十分的好奇~ 当然广州的同学看我拍雪景也很好奇 ~ 还有一次实验室和厦门大学一起合作做项目,我和一个厦门大学的博士都要了一份豆腐脑,我加韭菜花,他加糖,我两面对面坐着都觉得对方的不能吃。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-1e3954bc-0b41-4302-851c-d07d382040cd.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/guangzhou-1e3954bc-0b41-4302-851c-d07d382040cd.png) >作者:大白,转载链接:[https://mp.weixin.qq.com/s/uZQ8p0ytsQFXzt5ppzx6fA](https://mp.weixin.qq.com/s/uZQ8p0ytsQFXzt5ppzx6fA) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/hangzhou.md b/docs/cityselect/hangzhou.md similarity index 89% rename from docs/src/cityselect/hangzhou.md rename to docs/cityselect/hangzhou.md index 7a0589b3eb..cc158e4680 100644 --- a/docs/src/cityselect/hangzhou.md +++ b/docs/cityselect/hangzhou.md @@ -1,20 +1,19 @@ --- -shortTitle: 杭州 category: - 求职面试 tag: - 城市选择 --- -# 杭州都有哪些值得加入的IT互联网公司? +# 杭州有哪些顶级的互联网公司? 大家好,我是二哥呀!上期发了南京的互联网公司后,杭州的呼声非常高。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-1.png) -有小伙伴在公众号后台私信催,还有小伙伴在二哥的《二哥的Java进阶之路》的开源专栏提交了 issue,那今天必须得来整一波了。 +有小伙伴在公众号后台私信催,还有小伙伴在二哥的《Java 程序员进阶之路》的开源专栏提交了 issue,那今天必须得来整一波了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-2.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-2.png) 杭州的互联网公司比较多,我先列举一些大家瞅瞅(部分数据来源于好朋友 Carl 的统计)。 @@ -78,7 +77,7 @@ tag: ### 字节跳动 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-3.png) - 「基本情况」 :字节总部在北京,在上海、深圳、杭州、广州、成都等地都有办公室。去年 6 月,抖音电商落户杭州。 - 「业务方向」 :抖音电商、抖音餐饮、字节跳动广告业务、字节跳动本地生活 @@ -89,7 +88,7 @@ tag: ### 阿里系 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-4.png) - 「基本情况」 :阿里系占据了杭州互联网的一片天,相关联的企业是在是太多了。 - 「业务方向」 :达摩院、淘宝、菜鸟、钉钉、飞猪、盒马、支付宝、夸克、UC、书旗小说...... @@ -101,7 +100,7 @@ tag: ### 京东 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-5.png) - 「基本情况」 :京东在杭州也有招聘,不过,招聘的岗位比较少。 - 「业务方向」 :京东金融、京东云 @@ -112,7 +111,7 @@ tag: ### 网易 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-6.png) - 「基本情况」 :网易在杭州有一个研究院,成立于 2006 年 6 月。 - 「业务方向」 :⽹易杭州研究院,简称“杭研”。杭研是⽹易内部的基础技术研发中⼼和前沿技术研究中⼼,在云计算、⼤数据、安全、⼈⼯智能等⽅⾯进⾏前沿技术研究、关键技术攻关和基础技术平台研发,服务⽹易系游戏、邮箱、⾳乐、电商、新闻、在线教育等产品,触达近 10 亿⽤户。 @@ -123,7 +122,7 @@ tag: ### 华为 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-7.png) - 「基本情况」 :华为在杭州有一个研究所。 - 「业务方向」 :智能摄像机、云服务 @@ -135,7 +134,7 @@ tag: ### 中移杭研 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-8.png) - 「基本情况」 :中国移动的一个全资子公司,2014 年在杭州成立。 - 「业务方向」 :统一认证、融合通信、魔固云 @@ -146,7 +145,7 @@ tag: ### 之江实验室 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-9.png) - 「基本情况」 :之江实验室是浙江省委、省政府贯彻落实科技创新思想,深入实施创新驱动发展战略的重大科技创新平台。 - 「业务方向」 :智能感知、智能计算、智能网络、智能系统 @@ -157,7 +156,7 @@ tag: ### 同花顺 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-10.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-10.png) - 「基本情况」 :同花顺成立于 2001 年,总部位于杭州未来科技城,是国内第一家互联网金融信息服务业上市公司。同花顺作为一家互联网金融信息提供商,致力于为各类机构提供软件产品和系统维护服务、金融数据服务和智能推广服务,为个人投资者提供金融资讯和投资理财分析工具。 - 「业务方向」 :旗下有多款热门投资理财类 APP。 @@ -168,7 +167,7 @@ tag: ### 51 信用卡 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-11.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-11.png) - 「基本情况」 :51 信用卡(母公司为杭州恩牛网络技术有限公司),公司创立于 2012 年,是一家服务于中国亿万信用卡用户的互联网金融公司。 - 「业务方向」 :旗下有“51 信用卡管家”、“51 人品”、“51 人品贷”等 APP @@ -180,7 +179,7 @@ tag: ### 蘑菇街 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-12.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-12.png) - 「基本情况」 :2011 年,蘑菇街正式上线,2016 年 1 月与美丽说战略融合,公司旗下包括:蘑菇街、美丽说、uni 等产品与服务。 - 「业务方向」 :电商 @@ -192,7 +191,7 @@ tag: ### 有赞 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-13.png) - 「基本情况」 :有赞,原名口袋通,2012 年 11 月 27 日在杭州贝塔咖啡馆孵化成立,是一家主要从事零售科技 SaaS 服务的企业,帮助商家进行网上开店、社交营销、提高留存复购,拓展全渠道新零售业务。2014 年 11 月 27 日,口袋通正式更名为有赞。2018 年 4 月 18 日,有赞完成在港上市。2019 年 4 月,腾讯领投有赞 10 亿港元融资。 - 「业务方向」 :SaaS 服务(帮助商家网上开店、社交营销......)、PaaS 云服务。 @@ -204,7 +203,7 @@ tag: ### Zoom -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-14.png) - 「基本情况」 :Zoom 是一位美国华人企业家创办的公司,主营业务就是提供视频会议服务。总部位于硅谷,国内的话,杭州、苏州、合肥均有研发中心。 - 「业务方向」 :视频会议 @@ -216,7 +215,7 @@ tag: ### Cisco(思科) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-15.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-15.png) - 「基本情况」 :思科系统公司(Cisco Systems, Inc.),简称思科公司或思科,1984 年 12 月正式成立,是互联网解决方案的领先提供者,其设备和软件产品主要用于连接计算机网络系统,总部位于美国加利福尼亚州圣何塞。 - 「业务方向」 :路由器、交换机等网络基础设施 @@ -234,13 +233,13 @@ tag: 顺带再聊聊杭州的房价、教育吧。杭州的房价不忍直视啊!!!涨的实在是太厉害了! -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-16.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-16.png) 杭州只有一所 985,也就是浙江大学。除了浙江大学之外,浙江省没有 211 院校。不过,杭州电子科技大学虽然不是 211,但是实力很强,外界也很认可。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-17.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/hangzhou-17.png) >作者:大白,转载链接:[https://mp.weixin.qq.com/s/hrL2tqXHT5AjOqrQlRhR-w](https://mp.weixin.qq.com/s/hrL2tqXHT5AjOqrQlRhR-w) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/nanjing.md b/docs/cityselect/nanjing.md similarity index 82% rename from docs/src/cityselect/nanjing.md rename to docs/cityselect/nanjing.md index 0e5134dcfc..8325e6be1c 100644 --- a/docs/src/cityselect/nanjing.md +++ b/docs/cityselect/nanjing.md @@ -1,12 +1,11 @@ --- -shortTitle: 南京 category: - 求职面试 tag: - 城市选择 --- -# 南京都有哪些值得加入的IT互联网公司? +# 南京有哪些靠谱的互联网公司? @@ -14,7 +13,7 @@ tag: 有句话说的很对,“安徽不能没有南京,就像东北不能没有三亚”。不过调研中也发现,对于程序员来说,想要在南京留下也不是件很容易的事情,因为南京程序员的工作机会只能算一般,薪资水平这两年许多大公司选择在南京设立分部后才带起来一些,但是南京的房价已经很高了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-707d074e-def7-4cd3-af0a-84e46a0929a9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-707d074e-def7-4cd3-af0a-84e46a0929a9.png) 提一下哈,在这个系列里说的校招薪资都是常规招聘计划的薪资,说的常规招聘计划给出的 offer 包含普通 offer,sp (special offer) 和 ssp。不包含那些比如华为天才少年、美团北斗之类的。那种神仙 offer 怎么样我这种凡人还真不了解。这些招聘的薪资也都是针对本科生和研究生的。如果你是博士,薪资方面和公司都好商量。 @@ -24,7 +23,7 @@ tag: ### 阿里巴巴 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f55e084c-f731-419b-af71-278ac8e7b15c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-f55e084c-f731-419b-af71-278ac8e7b15c.png) 校招薪资:招的人不多,具体薪资情况不太清楚。据说和总部薪资水平差不多。阿里的薪资水平可以参考下[链接](https://mp.weixin.qq.com/s/3c-a7GpkTzAU2DFWSwN4lw)。 @@ -32,7 +31,7 @@ tag: ### 字节跳动 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f945934a-3ab8-4272-a8b2-7001ea396377.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-f945934a-3ab8-4272-a8b2-7001ea396377.png) 校招薪资:字节在南京的薪资是字节在北京上海深圳薪资的 0.9。感觉薪资水平挺不错了。今年字节校招薪资也可以点这个[链接](https://mp.weixin.qq.com/s/3c-a7GpkTzAU2DFWSwN4lw)查看。 @@ -40,7 +39,7 @@ tag: ### 荣耀 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-8470d707-53c6-4c07-8160-50759ad9d50b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-8470d707-53c6-4c07-8160-50759ad9d50b.png) 校招薪资:月薪 17k - 27k(对应13-15级),每年 14-16 薪。 @@ -50,7 +49,7 @@ tag: ### 华为 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f8ba1f60-c3bc-450b-b833-54db9bfdb807.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-f8ba1f60-c3bc-450b-b833-54db9bfdb807.png) 校招薪资:13级月薪 20-21k,14 级 23-25k,15级月薪 26-27k。每年 14-16 薪。16级我就不清楚了。 @@ -58,7 +57,7 @@ tag: ### 小米 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-bfb9cd80-f9b5-4ea7-b4f6-a15e54cf3c4b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-bfb9cd80-f9b5-4ea7-b4f6-a15e54cf3c4b.png) 校招薪资:小米的薪资是挺抠,不过放在南京来看也还行。看校招薪水上南京的薪资爆料今年有 11k,14k,16k,18k,19k,21k,23k 几个档。11k,14k基本都是本科生,硕士做开发岗的大部分是16k,18k,19k这几个档。20k 以上的基本都是做算法的。 @@ -66,7 +65,7 @@ tag: ### Vivo -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-7d676f6f-b081-4eb7-a2ce-3590a04b2fcd.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-7d676f6f-b081-4eb7-a2ce-3590a04b2fcd.png) 校招薪资:南京这边目前看到给应届生的价钱分为月薪17k,20k,24k三个档,,每年15薪。额外每个月有 1500 的租房补贴。住房公积金5%。 @@ -74,7 +73,7 @@ tag: ### OPPO -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-9ad6c7fe-c486-4628-8e07-f21b3e2cac6d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-9ad6c7fe-c486-4628-8e07-f21b3e2cac6d.png) 校招薪资:具体薪资不太清楚,大致应届生月薪在 20k 上下,每年 14 -15 个月薪资。 @@ -82,7 +81,7 @@ tag: ### 中兴 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-094809df-30b0-40c6-a1a5-f1e0a5329b6a.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-094809df-30b0-40c6-a1a5-f1e0a5329b6a.png) 校招薪资:中兴常规计划(指的是普通 offer)的月薪基本都在20k 以内,蓝剑计划年薪会在 40w 以上。 @@ -90,7 +89,7 @@ tag: ### 360 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-0bec3704-902d-4b8b-90b7-4ab5fc28a79d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-0bec3704-902d-4b8b-90b7-4ab5fc28a79d.png) 校招薪资:没调查清楚,年薪差不多 23w 左右。 @@ -98,7 +97,7 @@ tag: ### 深信服 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-5041109f-e9d8-4419-9f41-466f93708c53.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-5041109f-e9d8-4419-9f41-466f93708c53.png) 校招薪资:开发岗有3个档,月薪分布在15.6 - 22.8k 这个区间,算法岗也是三个档,月薪分布在19 - 25.2k 这个区间。每年13 - 15 薪 @@ -106,7 +105,7 @@ tag: ### 趋势科技 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f60ae6ee-13d7-4efc-bd5a-a0334b76c6e3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-f60ae6ee-13d7-4efc-bd5a-a0334b76c6e3.png) 校招薪资:看到校招薪资爆料有三个档,分别是年薪 20w,23.5w 和 25w。 @@ -114,7 +113,7 @@ tag: ### 亚信安全 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-97e8edd1-2a4d-492c-9039-febbae881c05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-97e8edd1-2a4d-492c-9039-febbae881c05.png) 校招薪资:基本上月薪在15-18.5k 这个区间,每年 13- 15薪。 @@ -122,7 +121,7 @@ tag: ### SHEIN -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-fc7ddb5b-cbc1-482f-9087-ea9e3e136795.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-fc7ddb5b-cbc1-482f-9087-ea9e3e136795.png) 校招薪资:目前看校招薪资爆料月薪基本集中在16-23k,有个说自己 27k 的不知道真的假的(如果是真的,感觉 SHEIN 大部分老员工要RUN了)。每年14薪。 @@ -130,7 +129,7 @@ tag: ### 满帮集团 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-ec98654b-0bcf-4e96-bb52-fe7ebd2a6c55.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-ec98654b-0bcf-4e96-bb52-fe7ebd2a6c55.png) 校招薪资:校招薪水上的爆料开发月薪基本都在 20-30k 这个区间,算法基本都在 27-36k 这个区间。大部分人每年 14 薪。 @@ -140,25 +139,25 @@ tag: ### 中电系列 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-21fd091d-8afd-4cde-8fbd-a78b3c805085.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-21fd091d-8afd-4cde-8fbd-a78b3c805085.png) 南京有中电 28 所、中电 14 所、中电 841 所。有一些读者跟我说想去研究所,想法是去研究所可以工作生活平衡。但是现在有很多的研究所其实很忙的,工作生活平衡的研究所尤其不包括南京这几个。841 所还不是特别清楚。大部分互联网公司和中电 28 所以及 14 所 比起来还真是弟弟,不过根据南京的薪资的水平来说,这两个所得福利待遇算是顶级了。刚毕业的应届生进去工资大概每月到手是一万出头吧,有食堂有宿舍,出差会有出差补贴,差不多一天三四百,这几个所出差会非常的多。 ### 南瑞集团(国网电科院) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-aff2d53a-95b6-4654-baac-ebc2ba39fc25.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-aff2d53a-95b6-4654-baac-ebc2ba39fc25.png) 南瑞集团是国家电网直属的科研企业单位,主要做电力系统自动化相关设备,所以会招很多程序员。年薪 14 万左右。南瑞要给各个省公司做电力软件,所以出差的情况会非常多。面试上南瑞以后你还没有国家电网编制,想要编制还要去参加电网的考试。感觉性价比一般,不如去省公司,听说省公司工资还挺高的。 ### 运营商 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-6f4cd4bc-e1da-496c-bf00-7ef2e6089dee.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-6f4cd4bc-e1da-496c-bf00-7ef2e6089dee.png) 几个运营商里只有联通在南京有研究院专门做软件相关,除此之外想去运营商就只有省公司了。一些运营商的子公司我就不多介绍了,我是感觉国企的子公司性价比一般,既没有国企的稳定也没互联网的高薪。 ### 烽火 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-d16dd7fe-f7a0-4f70-a016-f88350a56fa7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-d16dd7fe-f7a0-4f70-a016-f88350a56fa7.png) 简单说下烽火,下面有烽火星空还有烽火通信。是国企,不过感觉一直处于一个乙方的位置。项目很多是涉密项目,但是我劝大家尽量少碰涉密类型的项目。如果手头没大公司可以去烽火锻炼下,如果有其它大公司的 offer,我感觉还是优先其它大公司了。 @@ -170,18 +169,18 @@ tag: 南京的房价还是比较高的,比较好的区域的房价直逼一线城市了,对于刚毕业的学生来说压力很大了,感觉这个房价应该逼走不少人。 另外南京物价也不低噢。下面这张图是网上找的,不一定准,可以当作参考。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-b5247968-f8fa-445a-b83e-7d88fbb056c7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-b5247968-f8fa-445a-b83e-7d88fbb056c7.png) ### 教育 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-61b6b641-72d1-45d2-be1e-463c7d1d7f9c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-61b6b641-72d1-45d2-be1e-463c7d1d7f9c.png) 南京高校资源方面很强。南京大学、东南大学、南航、河海大学、南理工等等都是很不错的学校。另外南邮、南京工业大学这些双非学校的计算机也很强。感觉近几年各个大厂陆续在南京设分布也是看中了南京有大量的计算机相关专业的毕业生。 江苏高考题的难是出了名的,当然江苏省的中小学教育资源也很优质,所以在教育这方面选择南京还是不错的。 ### 交通&风景&气候&美食 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-9da3efca-ceb7-4685-9932-6613ea2ea1c6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/nanjing-9da3efca-ceb7-4685-9932-6613ea2ea1c6.png) 南京拥堵程度是上过全国前十榜单的,我租房是喜欢宁愿住的小一点差一点也尽可能离上班地方近一点,我比较抗拒通勤。宏观上来说,南京的地理位置特别好,离苏州、杭州、上海都很近,另外离安徽也很近,这也是安徽人喜欢往南京跑的原因。 @@ -194,4 +193,4 @@ tag: >作者:大白,转载链接:[https://mp.weixin.qq.com/s/CfZ1CEmtPOP4TAwAs8Ocrw](https://mp.weixin.qq.com/s/CfZ1CEmtPOP4TAwAs8Ocrw) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/qingdao.md b/docs/cityselect/qingdao.md similarity index 77% rename from docs/src/cityselect/qingdao.md rename to docs/cityselect/qingdao.md index f100d03eda..ca5138be3d 100644 --- a/docs/src/cityselect/qingdao.md +++ b/docs/cityselect/qingdao.md @@ -1,12 +1,11 @@ --- -shortTitle: 青岛 category: - 求职面试 tag: - 城市选择 --- -# 青岛都有哪些值得加入的IT互联网公司? +# 青岛有牛逼的互联网公司吗? @@ -14,7 +13,7 @@ tag: 当时我妹还在沙滩上留了一张“沉默王二在青岛”的印迹。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-938dc8a2-9b47-4e12-aa45-79c2caf94f04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-938dc8a2-9b47-4e12-aa45-79c2caf94f04.png) 按照我妹的说法,“青岛真是一座网红城市,美女是真多!” @@ -26,13 +25,13 @@ tag: ### 海尔 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-e656449e-a34f-4915-a1cb-459208d23e34.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-e656449e-a34f-4915-a1cb-459208d23e34.png) 海尔在青岛多年了,曾经有段辉煌期的。我舍友带着我在山东哈酒时,一个老大哥在酒桌上就跟我们聊起来,说他当初在海尔买家都是要请他们吃饭才卖给产品的。不过近些年海尔就不太行了,但依然在青岛是个工作上的好选择。海尔 Java 岗位给应届 985 硕士的报价是年薪 14 万。本科生相对少一点,年薪大概在 8万 到 12万 左右。我路过海尔时感觉海尔的园区还修的挺漂亮的。另外说一句,感觉海尔卡奥斯还行,在工业互联网领域做的还可以。 ### 海信 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-9645cf8d-d9dc-4de7-a192-bdb9a447ec49.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-9645cf8d-d9dc-4de7-a192-bdb9a447ec49.png) 我之前总觉得海信和海尔有某种联系,不过我查了下并没有,路过海信的园区时觉得海信也修的挺漂亮的。一个 211 硕士爆料的薪资是每月 10k,然后一年 13 个月工资。福利待遇方面感觉还可以,前两年有免费宿舍,交通补贴每月 200,住房补贴 800 每月,可以领三年。试用期 6 个月,试用期间 90% 薪资,免费班车,公积金 10%,社保全额。工作早9晚6,中午休息一小时。 @@ -40,43 +39,43 @@ tag: ### 光大银行青岛研发中心 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-7351e8f8-90d5-4f3c-998a-703b36ec1fca.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-7351e8f8-90d5-4f3c-998a-703b36ec1fca.png) 待遇听说还不错,以前是归分行管,现在归总行管。青岛这边主要开发云缴费业务。据说是有末尾淘汰机制。近几年好多员工都跑到青岛银行了。 ### 青岛银行 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-cc5091a0-28ad-4439-8718-a6b79e80ef83.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-cc5091a0-28ad-4439-8718-a6b79e80ef83.png) 青岛银行总行的技术岗。薪资不算高,有薪资爆料是年薪12w,不过算是摸鱼的天堂了,活大部分是外包干。 ### 青岛凯亚 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-9fcaebb9-fcf0-4b34-8302-c0be931ae1ce.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-9fcaebb9-fcf0-4b34-8302-c0be931ae1ce.png) 在青岛软件企业能算中上游了,不过全靠同行衬托。加上各种补贴,一年的工资差不多是是十二三万吧。试用期工资打五六折。技术还可以,不过有人爆料说他加班还挺多的。 ### 青岛鼎信 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-f941c172-1ff2-40ce-bea0-8cb52a030bf3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-f941c172-1ff2-40ce-bea0-8cb52a030bf3.png) 主要做通信的公司,一个应届硕士的爆料是税前13k,每年13个月的工资,有食堂,每个月600的饭补。 ### Yeelight -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-7f88980b-d21e-4b27-9306-f911eb8a02e6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-7f88980b-d21e-4b27-9306-f911eb8a02e6.png) Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330人,研发人员超过一半,主要做智能照明,有小米的投资。听说团队氛围还不错,比较重视技术。我在网上没找到校招待遇的介绍,社招的话 BOSS 招聘 3-5年 Java 经验的报价是 12k-24k。 ### 中车四方 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-f75fc5b3-2587-42d5-a80f-592a49c515eb.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-f75fc5b3-2587-42d5-a80f-592a49c515eb.png) 中车四方是国有控股公司,薪资待遇在青岛算中上。也招程序员。应届毕业生是6个月试用期,转正以后每年工资税前差不多15w左右。不过他这个工资构成和别人不一样呀。每月税前一万左右的工资好像其中六千左右是绩效,然后每年年中会发一个业绩奖金,不同部门的奖金差别很大。 ### 中电41所 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-53f614bb-d10d-4ae2-9f83-5911ef8ddead.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-53f614bb-d10d-4ae2-9f83-5911ef8ddead.png) 说实话,我一直对中电系列的研究所印象不是很好,不过听说中电28所的薪资后对中电系列的印象有所改观(这里是闲扯一句哈,中电28所在南京,和青岛没啥关系)。注意41所得总部是在安徽,青岛只是一个分部,我了解到是加班会比较严重。 @@ -88,7 +87,7 @@ Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330 青岛房均价两万多了吧。崂山和市南比较贵,其它地相对好一些。下面这张图有各区的房价,不是太准,不过可以参考下呀。感觉程序员在北京干几年,攒个首付,然后回青岛找个相对稳定的公司上班,买房压力还不是很大(除了买崂山和市南啊)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-a228e29e-363f-44a3-b8cc-6cdd18d34b9f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-a228e29e-363f-44a3-b8cc-6cdd18d34b9f.png) ### 教育 @@ -96,7 +95,7 @@ Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330 大学方面,山东大学在青岛是有校区的,虽然离市区是真的远,但是人家有地铁呀。我记得坐那趟地铁去山东大学青岛校区找我舍友时穿越崂山,窗外的风景是真的漂亮。中国海洋大学也是一所985,校区是真的漂亮。中国石油大学是一所211,也挺不错的。另外像青岛大学、青岛理工大学、山东科技大学这些院校也是不错的。这几年各个高校也在青岛修建了研究院,感觉还是挺有发展前景的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-b37788d7-c33d-475a-bc69-05927bf0825d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-b37788d7-c33d-475a-bc69-05927bf0825d.png) 高中方面,青岛二中、青岛一中、青岛五十八中、青岛九中、即墨一中等都非常好。不过山东高考的压力是真的大,感觉我大学和读研时身边的山东同学,当年高考都是英雄般的人物。 @@ -104,11 +103,11 @@ Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330 青岛是真的挺美的,感觉在青岛既能感觉到现代化大都市的感觉、又能体验到民国风情还能体验到欧式风情。另外我同学带我在青岛转时,经常走着走着就见到了大海,这让我这个多年身居内陆,没见过海的少年来说还是挺欣喜的。感觉青岛比西安冷很多,今年4月西安已经很热了,花都开了,但是去了青岛以后发现柳树才刚有一点点绿,还挺凉快的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-d619e60b-732b-497f-a4a0-ebbad7ce1ce9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-d619e60b-732b-497f-a4a0-ebbad7ce1ce9.png) 青岛的海鲜是挺丰富了,让我这个很少吃海鲜的少年一次吃了个够,还买了些海鲜给家里面寄了点。果然内陆的海鲜吃起来和沿海城市的海鲜差好多,另外山东菜是真的能倒酱油。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-ce0b22f6-0b52-425c-a5d0-4eefe52706f6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/qingdao-ce0b22f6-0b52-425c-a5d0-4eefe52706f6.png) ### 交通 @@ -117,4 +116,4 @@ Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330 >作者:大白,转载链接:[https://mp.weixin.qq.com/s/8QQvOrkG3Vdjj3GxP1zxBQ](https://mp.weixin.qq.com/s/8QQvOrkG3Vdjj3GxP1zxBQ) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/shenzhen.md b/docs/cityselect/shenzhen.md similarity index 95% rename from docs/src/cityselect/shenzhen.md rename to docs/cityselect/shenzhen.md index ec27cf797a..8ff60726c4 100644 --- a/docs/src/cityselect/shenzhen.md +++ b/docs/cityselect/shenzhen.md @@ -1,12 +1,11 @@ --- -shortTitle: 深圳 category: - 求职面试 tag: - 城市选择 --- -# 深圳都有哪些值得加入的IT互联网公司? +# 深圳有哪些牛批的互联网公司? @@ -83,5 +82,5 @@ AI独角兽公司 >作者:代码随想录,转载链接:[https://mp.weixin.qq.com/s/hBU-eEUq8aN5PWwdZFmC4g](https://mp.weixin.qq.com/s/hBU-eEUq8aN5PWwdZFmC4g) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/suzhou.md b/docs/cityselect/suzhou.md similarity index 83% rename from docs/src/cityselect/suzhou.md rename to docs/cityselect/suzhou.md index cbaa3f299e..4750d0a770 100644 --- a/docs/src/cityselect/suzhou.md +++ b/docs/cityselect/suzhou.md @@ -1,12 +1,11 @@ --- -shortTitle: 苏州 category: - 求职面试 tag: - 城市选择 --- -# 苏州都有哪些值得加入的IT互联网公司? +# 想搬去苏州生活了。。 @@ -22,7 +21,7 @@ tag: 有句话叫做上有天堂,下有苏杭,不过有一说一,互联网环境方面苏州和杭州感觉还是有点差距。但是个人觉得还是不错的,而且从 18 年左右开始,苏州的互联网环境开始有了明显的改善。生活方面无论是苏州的地理位置,还是人文环境,都是十分适合居住生活的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-e780c8c6-6e3f-4ba0-a91b-5cf572ee2d54.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-e780c8c6-6e3f-4ba0-a91b-5cf572ee2d54.png) 老规矩,下面我们还是按照程序员的工作机会和生活环境来介绍苏州。 @@ -32,7 +31,7 @@ tag: ### 微软 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-e18a11b0-2667-4e27-b93f-a05dad122e76.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-e18a11b0-2667-4e27-b93f-a05dad122e76.png) 微软可以说是程序员在苏州最好的工作机会了。微软近些年也一直扩大在苏州的投资力度,目前微软苏州研究院的研发团队已经有 2000 人左右了,并且还在近一步扩建。2022 年微软将启动微软苏州三期新大楼的建设,建成后预计微软在苏州的研发团队会达到5000人。所以,目前想拿微软苏州的 offer 的难度,相比于拿北京上海等其它几个工作地 offer 的难度会低一些,大家抓紧呀。 @@ -44,55 +43,55 @@ tag: ### ZOOM -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-ce9a78ef-6a18-4adf-8a4f-de7b7fa34b45.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-ce9a78ef-6a18-4adf-8a4f-de7b7fa34b45.png) 在苏州,Zoom是除微软外的另一个好选择。Zoom 曾经被评为全球最佳雇主。根据 zoom 员工的说法,zoom的技术水平大概和国内二三线厂的水平差不多。薪资方面,比国内互联网也要低一些,看到一个校招薪资的爆料,一个做前端的硕士在苏州zoom的薪资是17k * 14。额外有 7 万美元的股票,分 4 年发完。虽然 base 薪资方面相对低一些,但是 zoom 工作是真的爽呀,没有 996,没有 kpi,办公环境好,无限零食和水果供应,这还要啥自行车。 ### 华为 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-8bd7e5d1-1984-43f7-84e1-b02141160a37.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-8bd7e5d1-1984-43f7-84e1-b02141160a37.png) 华为在苏州的研究院设立时间不长。华为的苏研院的加班在各地所有的研发中心中是排的上号的。华为各地研发中心的忙碌程度差不多是成都 》= 西安 》= 苏州 > 武汉 > 东莞 > 杭州 > 南京 > 深圳 > 北京 > 上海(整体感觉是这样,每个研发中心各部门的加班情况也不同)。不过华为有一个好处就是你挣的钱能和你的付出成正比,所以想快速挣钱的还是建议去。 ### 360 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-85547b36-15da-443f-b7f2-878bfd06e997.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-85547b36-15da-443f-b7f2-878bfd06e997.png) 360 来苏州时间不长,2018年才逐渐开始在苏州设立开发部门。目前 360 苏州包含了未来安全研究、360政企、安全等部门。目前 360苏州的薪资水平是北京上海薪资水平的 8 折,看到 21 年毕业的一个应届硕士爆料月薪是 16k,工资有点少,不过感觉在苏州也不错呀。 ### Momenta -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-c4c83915-04b5-4660-9bad-32989a65116d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-c4c83915-04b5-4660-9bad-32989a65116d.png) 一开始我对 Momenta 还真没什么了解,直到我有一天突然在校招薪水上搜了下 Momenta,然后我不由得直呼卧槽,这特么是给应届生的薪资?认真的? -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-ff400245-2fe0-437e-8dd3-aa9eed650e99.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-ff400245-2fe0-437e-8dd3-aa9eed650e99.png) 然后我详细的了解了下这家公司,这是一家做自动驾驶的公司,创立时间也不长,2016年创立的。不过据说这家公司加班特别猛,有的组 10 10 5,有的组 996,有的组比 996 还要累。Momenta 现在能给的起这么高的工资,实力还是有的,不过对于发展的前景来说,领域这么垂直的公司风险性是比较大的。 ### 企查查 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-d4bcbf1d-f758-4267-b55f-7553d7673511.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-d4bcbf1d-f758-4267-b55f-7553d7673511.png) 跟上面的公司相比,企查查算是一个小而美的公司。企查查是苏州的本地企业,目前来说口碑还是很不错的。目前企查查的业务很赚钱,自己的大楼也差不多修好了。没有强制加班,全额社保和公积金。我没有查到企查查的校招薪资,看 boss 上的社招薪资水平不算太高,只能算还可以了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-c7dbed8d-c471-4969-bfc4-badf8e0ac346.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-c7dbed8d-c471-4969-bfc4-badf8e0ac346.png) ### 收钱吧 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-31051fbc-a573-4328-904b-6915bfba2323.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-31051fbc-a573-4328-904b-6915bfba2323.png) 这个公司大家应该都听说过,每次你用支付宝或者微信给商家付钱时都能听到“收钱吧到账xx元”。收钱吧的技术不错,加班也比较少,收钱吧的风评不错。不过就是工资相对给的少点,月薪 17k 左右(在苏州也还行了),每年14 个月的月薪。 ### 中国移动苏州研究院 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-1bb72f74-dba6-420f-8c37-0a9e621b3561.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-1bb72f74-dba6-420f-8c37-0a9e621b3561.png) 中国移动苏州研究院又叫苏小研,其实犹豫了很久要不要把他放在推荐里。苏小妍在知乎上的争议很大,有说待遇福利很好的,也有狂喷的。苏小研肯定有国企的通病,这个我确认,但具体怎么样就靠大家自己判断了,贴两张知乎上对苏小研的评价,好坏要大家自己去具体确认了。这两张图评价完全是两个极端呀。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-b616269f-3717-4ea4-8c29-b4d5bcd448b4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-b616269f-3717-4ea4-8c29-b4d5bcd448b4.png) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-76122245-a8e1-44a8-a1f4-8ce7ab3c640d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-76122245-a8e1-44a8-a1f4-8ce7ab3c640d.png) 上面是我认为苏州的比较不错的程序员就业机会,其它的一些苏州比较成规模的提供程序员就业机会的公司就列在下面,并且快速的简单介绍一下。 @@ -120,7 +119,7 @@ tag: ### 教育 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-1b300d4a-1384-4dd2-a9a5-98221f1f6c46.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-1b300d4a-1384-4dd2-a9a5-98221f1f6c46.png) 高等教育方面。其实就苏州本身而言,高校并不多,只有苏州大学一所 211(苏大的自然语言处理挺强的)。但是前些年许多地理位置偏北的高校为了打造自己在南方的影响力,提高生源质量,都会选择在南方办学。其中一部分选择了深圳,另一部分选择了苏州。西交大、中科大、中国人民大学、东南大学、西工大等院校都在苏州设有校区。所以虽然苏州本地院校不多,但是苏州计算机软件相关的高校毕业生并不少。不过这种现象随着西工大太仓校区刚修好,异地办学就被叫停了,我瓜实惨~ @@ -130,11 +129,11 @@ tag: 相比于苏州的工资水平,其实苏州的房价也不低了,看网站上的新房均价大约两万五一平。但是和旁边的城市一比,那苏州的房价就比较香了。在苏州咬咬牙还是能考虑买房的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-60675498-4bfe-46d2-af70-da2f21add403.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-60675498-4bfe-46d2-af70-da2f21add403.png) ### 娱乐&交通&美食 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-c35c16ca-0bbf-489a-ba0b-9ecb6fef6905.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/suzhou-c35c16ca-0bbf-489a-ba0b-9ecb6fef6905.png) 娱乐方面苏州好玩的地方很多,传统的苏州园林风格和苏州现代风格都让人十分留恋。周末在苏州园林逛一逛真的很惬意。 @@ -147,4 +146,4 @@ tag: >作者:大白,转载链接:[https://mp.weixin.qq.com/s/cnYsZLudFOwv5EKYMsMh0Q](https://mp.weixin.qq.com/s/cnYsZLudFOwv5EKYMsMh0Q) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cityselect/xian.md b/docs/cityselect/xian.md similarity index 87% rename from docs/src/cityselect/xian.md rename to docs/cityselect/xian.md index 23ef78967a..5b720f2f70 100644 --- a/docs/src/cityselect/xian.md +++ b/docs/cityselect/xian.md @@ -1,12 +1,11 @@ --- -shortTitle: 西安 category: - 求职面试 tag: - 城市选择 --- -# 西安都有哪些值得加入的IT互联网公司? +# 西安有哪些不错的互联网公司? 2019 年国庆节的时候去了一趟西安,和朋友一起,开车去的,大概 4 个多小时的车程,从洛阳到西安,不算太远。 @@ -31,7 +30,7 @@ tag: **华为** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-4e82d879-bbe0-4bc5-a088-fadc84483d65.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-4e82d879-bbe0-4bc5-a088-fadc84483d65.png) 如果想在西安快速挣钱的话,华为几乎是最好的选择了,按照华为的工资水平在西安买房根本没有压力。华为在西安的建制很齐全,消费者、CloudBu、云核心等事业群以及`华为海思`,2012 实验室都有。但是因为去年美国对华为的打压,消费者、华为海思以及 2012 实验室这些之前很香的事业群目前日子都不太好过。 @@ -45,13 +44,13 @@ tag: **阿里巴巴** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-135b1f20-48ad-4295-bc50-40969ef023b0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-135b1f20-48ad-4295-bc50-40969ef023b0.png) 阿里巴巴还没大范围的在西安招人,情况目前还不太清楚。我问过阿里巴巴的员工,只知道目前在西安设点的部门是阿里云,需要半年在杭州上班,半年在西安上班,工资水平和杭州一致。不过目前招的量比较小,基本都是招高p,主要是做售前的业务架构设计,只有很少的校招名额。 **京东** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-c9b0621d-f56c-42a0-a618-f261466f06a4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-c9b0621d-f56c-42a0-a618-f261466f06a4.png) 京东把京东物流的团队设在了西安航天城,目测团队规模在几百人,工资水平大约是京东在北京工资的 80%。大家可以参考一下,京东去年在北京的部门,校招普通 offer 是年包 28 万,sp 是年包 32.9 万。 @@ -59,7 +58,7 @@ tag: **腾讯云** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-67aa194e-52b0-4acb-b22b-1c1f4c09fac8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-67aa194e-52b0-4acb-b22b-1c1f4c09fac8.png) 腾讯云是腾讯的全资子公司,目前对这里褒贬不一。校招薪资水平本科和硕士生大概是月薪 13-16k,每年 16 薪,大家可以参考一下,腾讯 21 届校招的薪资是 17-21.5k ,每年 18 薪。社招我看一个本科毕业四年经验的老哥拿到的年包是 33 万。腾讯云的职级和腾讯不是对齐的,并且业务比较边缘,这也是网上被喷的一个主要原因。目前西安腾讯云创立时间不久,加班强度是比较大的。 @@ -69,7 +68,7 @@ tag: **广联达** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-9180be40-265e-42e5-9a85-4d61193b8798.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-9180be40-265e-42e5-9a85-4d61193b8798.png) 广联达西安的部门在针对 21 届毕业生的校招中开出的薪资是很有诚意的。给应届硕士的 sp 是 19k x 15,普通 offer 是 17k x 15,本科生每月基本都在 13-17k 之间。广联达工作制度基本是 965 或者 975,每个月还有一天的带薪病假。这还要啥自行车?不过广联达曾经也有过黑料,大家自行上网了解。 @@ -79,7 +78,7 @@ tag: **360** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-327db5f0-4061-48c3-b672-b1670f98325f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-327db5f0-4061-48c3-b672-b1670f98325f.png) 360 在西安只有一个几十人的团队。没听说过这里有校招,社招的话 3 年以上经验的差不多能每月给到 20k 。有说在这里待的舒服的,也有喷的。 @@ -89,7 +88,7 @@ tag: **科大讯飞** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-3592f841-8390-4399-ae8a-94c4368541ea.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-3592f841-8390-4399-ae8a-94c4368541ea.png) 科大讯飞西安丝路总部主要是算法岗,不过我看网上喷的比较多。据说是活多钱少。 @@ -103,7 +102,7 @@ tag: **绿盟** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-b70c6624-f4f1-453f-9ec8-5db22661bb18.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-b70c6624-f4f1-453f-9ec8-5db22661bb18.png) 绿盟算是中型企业里比较香的,绿盟在安全领域还是比较强的。工资比较低,硕士校招才能给到 14k x 14,但是这家公司几乎不加班,员工的离职率也一直很低,我同学有违约中兴三方去绿盟的。 @@ -119,7 +118,7 @@ tag: **寒武纪** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-fdf08fc3-d216-4382-913d-5f8b78b8e7df.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-fdf08fc3-d216-4382-913d-5f8b78b8e7df.png) 寒武纪是做智能芯片的公司,背后站着中科院计算所。2019 年落户的西咸新区,相关信息比较少。我知道的是加班比较多,不过薪资也高。 @@ -133,7 +132,7 @@ tag: 感觉西安几家国企的性价比略低,工资不高,且大部分加班严重。校招应届硕士工资税前年薪基本都在 15w-20w 之间,本科生的年薪比硕士少 3w 左右。国企相较于私企涨薪会慢很多,不过相对稳定一些。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-e12acc1d-4b6d-4f2e-a352-7155b527fa07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-e12acc1d-4b6d-4f2e-a352-7155b527fa07.png) **中兴** @@ -145,7 +144,7 @@ tag: **联通** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-33ac9a8b-04d3-4adb-8482-e725e25bb2f9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-33ac9a8b-04d3-4adb-8482-e725e25bb2f9.png) 西安联通软件研究院,应届硕士的总包和移动差不多,大约 16-19 万。不过联通比较清闲,大部分部门都能下午六点就下班。目前在网上的评价相对好一些。 @@ -163,7 +162,7 @@ tag: **荣耀** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-19d21a8f-6a45-408a-9f0f-ae8dd40bdbb1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-19d21a8f-6a45-408a-9f0f-ae8dd40bdbb1.png) 荣耀目前的招人需求是很大的,工作强度未知,薪资水平目前是完全对标华为的。 @@ -181,7 +180,7 @@ tag: **农行软开** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-cfa06bd9-abd6-455e-bd5c-aef4c90f7edb.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-cfa06bd9-abd6-455e-bd5c-aef4c90f7edb.png) 农行软开目前是在西安工作的最好的几个选择之一,硕士每月到手 12k,本科生少几百块钱,年终奖 2-4 个月。大部分部门都能晚上 7 点以前下班,并且周末双休。目前农行软开有子公司化软件开发中心的计划,听消息说可以选择去子公司,也可以留在软开。目前西安的农行软开也越来越卷,大部分的 offer 都给了西电、西交、西工大这三个学校了,另外这三个学校的学生现在也不是想去农行就能去了。农行软开有个硬性规定是必须通过六级才能报。 @@ -207,7 +206,7 @@ tag: **邮政银行** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-946ce62c-0afc-48c8-b570-fab660d2ea13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-946ce62c-0afc-48c8-b570-fab660d2ea13.png) 邮储银行软件开发中心在西安刚成立,还不太确定。目前宣传是年包 28 万以上,工作强度目测比较大。 @@ -221,7 +220,7 @@ tag: **三星** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-31302c7c-7dcd-4f12-b624-872b4878670d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-31302c7c-7dcd-4f12-b624-872b4878670d.png) 大部分应届生都是(11-14)k \*13.5k。965 工作制。几乎都是芯片、运维相关岗位。 @@ -237,7 +236,7 @@ SAP 是一家做企业软件的德企,技术十分强大。硕士年薪 20 万 **Thougtworks** -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-167d941b-740e-4342-9d28-1ba753766398.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-167d941b-740e-4342-9d28-1ba753766398.png) 在 Thoughtworks 工作是很舒服的,开放式办公、扁平化管理、技术氛围浓厚。工资本硕都是 13k x 14。 @@ -253,7 +252,7 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW ### 研究所 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-872dc3ff-316d-4194-b549-f998b39feedf.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-872dc3ff-316d-4194-b549-f998b39feedf.png) 西安航空航天类的研究所特别多,我知道的招计算机方面的研究所有航天 504 所、771 所,航空 631 所、618 所、603 所,中电 20 所、39 所,兵器工业 203 所、204 所、205 所。 @@ -265,7 +264,7 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW ## 生活 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-980c455e-1858-4c52-8c4f-5c1b0ed996b4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-980c455e-1858-4c52-8c4f-5c1b0ed996b4.png) 陕西的资源基本都集中在西安,从人口上也能看出西安的资源有多集中。整个陕西三千多万人,在西安就有一千多万。并且这些年中央对西安的扶植力度越来越大。 @@ -273,23 +272,25 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW 西安的房价从 18 年到现在翻了一倍,但就目前的房价相较于其它同类型城市算是比较友好的。现在西安的房价最贵的在曲江、第二贵是高新区。其它地方的房价差不多一万六左右吧,不过今年的全运会过后可能会长一波。按照程序员的工资来说,在西安买房的问题不算很大,这也是程序员待在西安比较舒服的地方。对于程序员来说,租房的压力相对较小,我同学有在农行软开工作的,在附近租了一个一居室的开间四十平左右,一个月一千五,上班步行用不了十分钟。高新那边租房贵一些,你愿意合租的话压力也不大。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-b905fada-e4a5-4a0a-a704-cbf9f310be6c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-b905fada-e4a5-4a0a-a704-cbf9f310be6c.png) 西安住建前段时间出了二手房交易参考价格,我贴在下面大家可以参考下,不过这个价格感觉低于市场价了。我感觉这直接打了个八折~西安住建发布二手房交易参考价格的链接在这里 https://mp.weixin.qq.com/s/Gis7kIJklWygTseztydDaw +![image-20210720212848036](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-c22be12f-8426-487d-b256-96d7e8950762.png) +![image-20210720212821035](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-fb06e9b2-ba0b-46d3-b84a-5449a77dc6ae.png) ### 教育资源 西安的教育资源很好。高中教育资源方面,西安的名校众多。西工大附中、西安铁一中、高新一中、交大附中、陕师大附中这些学校在全国都是很有名的,另外还有一批在陕西省很有名的高中也很不错。大学教育资源方面,整个陕西有三所 985,西工大和西安交大都在西安,西北农林科技大学就在离西安不远的杨凌。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-d905fc96-b354-45fa-b6aa-cfb4e2171eb7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-d905fc96-b354-45fa-b6aa-cfb4e2171eb7.png) 另外,还有像西安电子科技大学、西北大学、陕西师范大学、长安大学、第四军医大学这些不错的 211,还有像西安邮电、西安理工、西安科技大学、西安工业大学等等这些不错的双非院校。西安每年产出的人才的数量是很庞大的。这是很值得西安自豪的一点,但是这也造成了一个问题,西安就业十分的内卷。有一个现象是陕西人都愿意在西安,不愿意出来,计算去外面上学的陕西人毕业也大部分都回到了西安,另外在西安上过学的也大部分留在了西安。西安的几个效益比较好的研究所、银行软开的应聘难度比在北京的同级别单位都难很多。内卷不仅表现在计算机,计算机算好的,我了解到目前好多西安的小学老师都敢只要 211 以上毕业的硕士生。 ### 医疗资源 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-e6f524ec-e353-48c2-b89a-cd2ca6373924.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-e6f524ec-e353-48c2-b89a-cd2ca6373924.png) 医疗资源方面西安也很给力,交大一附院、交大二附院、唐都医院、西京医院、红会医院都是放在全国都很强的,另外其他一批省内比较有名的医院也很不错。 @@ -297,7 +298,7 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW 西安的交通方面不敢恭维,堵车那是一绝,我的感觉是西安比北京都要堵。西安的地铁 21 年初新开了 3 条线路,目前共有 8 条线路才勉强够的上需求。每逢法定节假日,旅游的人都会把西安挤炸。说到旅游,近年来西安对游客的吸引力是越来越大,一方面西安在弘扬大唐文化,另一方面西安的美食也叫一个美滴很。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-e3de7963-bfcf-4b37-9306-ff9939202208.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/xian-e3de7963-bfcf-4b37-9306-ff9939202208.png) 另外,大唐不夜城这里的人是真的多,尤其是夏天,晚上的时候基本打不到车! @@ -311,8 +312,7 @@ ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtW 综上所述,西安目前正处在高速上升的阶段,互联网行业相对北上广深杭还有一定的差距,相比与成都也还稍差一点。但是西安绝对是有潜力的,并且目前西安的房价还是比较友好的。大家如果能选择在西安发展,生活幸福感会比较高。 -更多西安的信息,可以戳这个链接:[西安互联网](https://github.com/madawei2699/xian-IT) >作者:大白,转载链接:[https://mp.weixin.qq.com/s/s0Ub1CHC9eEi0YrqPrnRog](https://mp.weixin.qq.com/s/s0Ub1CHC9eEi0YrqPrnRog) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/cityselect/zhengzhou.md b/docs/cityselect/zhengzhou.md new file mode 100644 index 0000000000..646540fced --- /dev/null +++ b/docs/cityselect/zhengzhou.md @@ -0,0 +1,278 @@ +--- +category: + - 求职面试 +tag: + - 城市选择 +--- + +# 郑州有哪些不错的互联网公司? + + +首先我们来看工作机会! + +一位读者的评论我觉得特别的好,我贴到这里给大家看下: + +网名"咔嚓":作为在郑州工作的前端,表示遇见好厂的话,生活节奏还是很爽的,房租不贵,直接住公司旁边,通勤5分钟,不加班的话,每天晚上6点下班,双休。确实郑州互联网不强,但是,我们也不应该忘了生活本该有的样子啊。 + +## 工作机会 + +郑州的互联网资源还是比较匮乏的,究其原因,我觉得和教育资源的匮乏有非常大的关系。 + +教育资源极度匮乏导致好的企业不来,好的企业不来又导致人才外流,恶性循环。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-0cd5c4ac-4e67-4b06-b5a3-20fad836824b.png) + + + +### 数字郑州 + +![img](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-b88e7ee0-5ab7-40af-8d1d-16cd3eae7b97.png) + +这个是阿里和郑州的政府合作的,目前评价大家对数字郑州的评价很不错呀,薪资也挺给力的,大家可以看下 Boss 上数字郑州的招聘岗位以及薪资报价呀。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-b1ef6dc9-0859-47fd-9058-a504bf01cd5a.png) + +### 中原银行 + +![img](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-db7b6919-2158-4846-914f-f30e028c0234.png) + +中原银行的工资比较高,在郑州生活的话去中原银行是很不错的选择,不过想进中原银行的话,不是校招想进去有点难。薪资水平可以看下 Boss 上的招聘薪资水平。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-ce9e2a6e-8743-49b3-83aa-8719da78b7e1.png) + + + +### 浪潮 + +浪潮在郑州的研发中心法定节假日加班是有加班费的,但平时加班就没有加班费了,每月要求加够50个小时的班。薪资水平大家也可以参考下 Boss 上放出的招聘薪资水平。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-ef0fe9a0-eff2-4037-96d2-ab84a2f76491.png) + +### 新华三 + +新华三大部分情况下能双休,周末加班也有加班费,不过涨薪很缓慢。在网上看到一个帖子,有人问 offer 选西安中兴还算郑州新华三,中兴和华三的职工都在互相劝退,说这是一个送命题。薪资水平大家还是参考下Boss上的招聘薪资水平吧。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-caa46657-f65d-4965-b553-c9f4502f3cc9.png) + +![点击并拖拽以移动](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-62496f27-fe20-45c6-b116-90a38e607a09.jpg) + +### UU 跑腿 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-624dd61d-2b19-4cb6-b4b6-e1b813fd6baf.png) + +UU 跑腿主要提供同城送件服务,是郑州本土最大的互联网公司,隶属于郑州时空隧道信息技术有限公司,地址位于郑州市金水区。 + +UU 跑腿的工作环境以及各种福利都还算不错! + +面试的话,总体体验还不错,技术面试一般问的还比较全面。拿 Java 后端开发来说,像 SQL 优化、分布式、缓存这些一般都会问到。 + +薪资的话,看准网上的平均薪资是 10k 附近,其中后端开发的薪资在 14k 附近,前端开发的薪资在 10k 附近,软件测试的薪资在 10k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-b3a5d176-a10b-4637-a349-06197ee984a8.png) + +### 中原消费金融 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-3317ba70-8310-465c-b36d-60f8e215960c.png) + +河南中原消费金融股份有限公司是一家全国性非银行金融机构,地址位于郑州市郑东新区。 + +中原消费金融的办公环境非常不错,薪资福利相对也还不错。 + +整体面试体验不错,效率也非常高,像技术面试的话一般是三轮或者四轮。不过,中原消费金融比较看重学历,985/211 上岸的几率比较大。 + +薪资的话,看准网上的平均薪资是 16k 附近,其中后端开发的薪资在 17k 附近,前端开发的薪资在 16k 附近,软件测试的薪资在 14k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入,应该到不了这么高)。 + +注意:大家注意一个情况,中原消费的软件研发岗位大部分都搬迁到上海了,目前在郑州的大部分是行政岗位,只有少部分研发岗位。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-6de95df2-4c10-44af-9ddb-935cb358def6.png) + +### 刀锋互娱 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-aa02294b-e809-49b6-9e96-4ec44db676c3.png) + +刀锋互娱是一家专注游戏服务市场的互联网公司,2019 年完成 A+轮融资,平台注册用户量突破千万。 + +旗下比较出名的产品有租号玩、一派陪玩,都是和游戏领域相关的产品。相信比较喜欢玩游戏的小伙伴应该对这个两个产品有了解。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-ec100440-a50e-4f27-88e4-4b075cf12676.png) + +整体面试不是很难,薪资相对来说也还可以。 + +薪资的话,看准网上的平均薪资是 16k 附近,其中后端开发(C++)的薪资在 20k 附近,前端开发的薪资在 8.5k 附近,软件测试的薪资在 9.5k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-1b7640e6-deee-42f6-8df7-639555c5d236.png) + +### 新开普 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-ca1cf626-13aa-4081-b6c0-285893458dde.png) + +新开普也是郑州的一家本土互联网公司,成立于郑州高新技术产业开发区,主要做 NFC 近场移动支付、金融 IC 卡等业务。 + +新开普是目前国内一卡通行业唯一一家上市公司,已经为全国千所高校,千万名大学生提供服务。 + +技术面试的话,一般第一面是笔试,笔试之后会再问你一些相关的技术问题。 + +薪资的话,看准网上的平均薪资是 7.6k 附近,其中后端开发(Java)的薪资在 9k 附近,前端开发的薪资在 9k 附近,软件测试的薪资在 5.5k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-dca4c23e-7310-4a60-b338-25f3e92d3339.png) + +### 中移在线 + +中国移动旗下的一家“互联网”公司,实际不像是“互联网”公司。 + +对于技术开发来说,去中移在线一是对技术没有提升或者挑战,二是工资是真的低(看准网上的 Java 开发薪资在 8k 附近)。 + +真心不太建议去,除非你没有其他更好的选择。 + +我能想到唯一的优势可能是公司相对来说能提供给你的一个相对稳定的工作。 + +### 爱云校 + +爱云校常见于 2014 年,主要做的是在校教育这块,致力于通过 AI 建一所云上的学校。 + +单看公司所做的业务方向来说,发展相对来说还是不错的。不过,据说公司的管理真的是渣到了一定程度。 + +另外,根据大部分面试求职者的反馈来看,这家公司的整体面试体验比较差。 + +薪资的话,看准网上的平均薪资是 12k 附近,其中后端开发(Java)的薪资在 14k 附近,前端开发的薪资在 8k 附近,软件测试的薪资在 10k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-90273777-d5d0-424a-a8b0-e5df53455ef9.png) + +### 妙优车 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-b6ca12ab-ecb5-4b01-9cf3-21b22da0d67a.png) + +妙优车主要做的是汽车方面的业务,涵盖整车销售、汽车金融、汽车保险、汽车用品、汽车美容等方面。 + +公司发展一般,网上也有一些黑历史(可以自己查一下)。 + +不过,根据大部分面试求职者的反馈来看,这家公司的整体面试体验还是可以的。 + +薪资这块的一般偏上,看准网上的平均薪资是 11k 附近,其中后端开发(Java)的薪资在 12k 附近,前端开发的薪资在 10k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-491c2bc1-5cea-48e6-ad47-80eb92ddaa3d.png) + +### 腾河 + +腾讯是河南腾河网络科技有限公司的最大股东,第二大股东是河南日报。 + +主要做的业务方向是河南城市生活第一网。 + +工资比较低,另外,后端这块好像只招 PHP。 + +### 真二网 + +不得不说,这个名字有点东西! + +真二网创立于 2014 年,也是郑州本土的一家互联网公司,主要做 C2C 模式的 0 中介费真实二手房交易平台。 + +工资比较低,另外,后端这块好像只招 PHP。 + +### 硕诺科技 + +硕诺科技创立于 2014 年,总部位于上海,主要做物流软件系统高端定制的软件开发。 + +网上可以查到的消息比较少,感兴趣的小伙伴可以自己去查一下相关信息啊! + +另外,如果有小伙伴对这家公司比较了解,也可以在评论区说一下啊! + +### 科大讯飞 + +科大讯飞在郑州金水区有一个小分部,大部分招聘的都是和技术无关的岗位,不过也有一个 Java 开发岗。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-dc1517e7-c93b-457f-96d2-0871c17bf916.png) + +### 字节跳动 + +郑州也有字节跳动分部,不过都是和市场拓展与运营相关的岗位,像技术开发岗是没有的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-c7d11e68-2b85-4895-a3c5-44f6816ccfb2.png) + +类似的还有美团、华为、阿里巴巴等大厂,这些公司在郑州招聘的基本也都是非技术岗位。 + +### 其他 + +其他还有像郑州点读电子科技有限公司(旗下产品有咿啦看书)、羲和网络(河南唯一一家游戏上市企业)、米宅(中国知名的楼市自媒体,新三板上市企业)等互联网公司,感兴趣的小伙伴可以自行查阅相关信息呀! + +读者补充:海康威视、APUS、云鸟、亚信科技、牧原食品、小鱼易联、神州信息、云智慧都在郑州招开发工程师。 + +## 生活环境&生活成本 + +我们再来看看生活环境和生活成本。 + +### 房价 + +郑州的房价对于其发展来说还是比较贵的。当然了,相比于一线城市肯定还是要便宜很多的! + +以下房价数据来源于安居客,可以作为参考。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-95ec5784-5064-488c-bce8-4c076115a021.png) + +### 教育 + +郑州的教育资源极度匮乏!据统计郑州一共有 65 所高校,其中,本科 26 所,专科 39 所。 + +不过,211 院校仅有一所——郑州大学。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-03294872-3914-441b-ae6b-55200bce097a.png) + +是的!作为偌大的河南省的省会,国家历史文化名城,也就只有一所 211! + +我们来对比一下湖北省省会武汉,武汉 7 所 985/211 高校,分别是武汉大学、华中科技大学、中国地质大学、武汉理工大学、华中师范大学、华中农业大学、中南财经政法大学。 + +再来对比一下湖南省省会长沙,长沙有 4 所 985/211 高校,分别是国防科大、中南大学、湖南大学、湖南师范大学。 + +### 医疗 + +郑州的每万人床位数排名比较靠前。 + +另外,郑州市的优质医疗资源,在金水区、中原区、管城区比较集中; + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-6bbd8d11-dbe5-43c4-9354-b422b7fe1f77.png) + +### 本地居民 + +网上有很多“河南黑”,让很多人对河南的影响不好! + +实际情况可能并不是这样的!郑州本地居民不排外,绝大部分都特别老实,本本分分。 + +我去过很多城市,郑州人的友善程度我觉得是可以排在 Top 级别的! + +另外,郑州这边的居民还是比较恋家的。有很多在北上广混的还不错的人,最后也还是选择回来! + +### 交通 + +作为一个北方内陆城市,郑州可以说是一个“交通枢纽”。从郑州出发坐高铁,你去国内大部分地方都非常方便。 + +下图中的部分高铁线路正在修建,比如郑万高铁全线大概是 2021 年中旬通车。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-0a4d6b3c-a756-4509-a6fd-f3a65f018c84.png) + +郑州的地铁规划情况如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-264ab1f2-f355-466a-ab5d-2dc21668e888.gif) + +目前的话,郑州地铁有 1 号线、2 号线、3 号线、4 号线、城郊线、5 号线、14 号线一期 7 条地铁线。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-c5e61f03-efcb-4e56-b9bf-ff7bcec3dce0) + +### 美食 + +郑州的各种商业设施还是比较齐全的,有很多大型的商场,商场里面基本是样样俱全。 + +郑州好吃的还挺多的!去了郑州之后,一定要去喝胡辣汤,真的不要太好喝! + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cityselect/zhengzhou-757eed5b-4b81-4bb0-a0e1-b24e510cc707.png) + +本地的美食还有烩面、焖饼、烧鸡等等都非常不错。 + +巩义那边有一家花雕醉鸡真心不错,价格便宜,2 个人不到 100 元就能吃的很好!最关键的是味道真的好!!! + +像浙菜、豫菜、火锅串串这边都能找到比较好吃的店子,可以满足绝大部分小伙伴的味蕾。 + + + +>作者:大白,转载链接:[https://mp.weixin.qq.com/s/SU9drg2xJKcheIwJ6OSSBQ](https://mp.weixin.qq.com/s/SU9drg2xJKcheIwJ6OSSBQ) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/collection/arraylist.md b/docs/collection/arraylist.md new file mode 100644 index 0000000000..2461d6b90d --- /dev/null +++ b/docs/collection/arraylist.md @@ -0,0 +1,396 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java集合ArrayList详解 + + +“二哥,听说今天我们开讲 ArrayList 了?好期待哦!”三妹明知故问,这个托配合得依然天衣无缝。 + +“是的呀,三妹。”我肯定地点了点头,继续说道,“ArrayList 可以称得上是集合框架方面最常用的类了,可以和 HashMap 一较高下。” + +从名字就可以看得出来,ArrayList 实现了 List 接口,并且是基于数组实现的。 + +数组的大小是固定的,一旦创建的时候指定了大小,就不能再调整了。也就是说,如果数组满了,就不能再添加任何元素了。ArrayList 在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活。 + +Java 这门编程语言和 C语言的不同之处就在这里,如果是 C语言的话,就必须动手实现自己的 ArrayList,原生的库函数里面是没有的。 + +“二哥,**如何创建一个 ArrayList 啊**?”三妹问。 + +```java +ArrayList alist = new ArrayList(); +``` + +可以通过上面的语句来创建一个字符串类型的 ArrayList(通过尖括号来限定 ArrayList 中元素的类型,如果尝试添加其他类型的元素,将会产生编译错误),更简化的写法如下: + +```java +List alist = new ArrayList<>(); +``` + +由于 ArrayList 实现了 List 接口,所以 alist 变量的类型可以是 List 类型;new 关键字声明后的尖括号中可以不再指定元素的类型,因为编译器可以通过前面尖括号中的类型进行智能推断。 + +如果非常确定 ArrayList 中元素的个数,在创建的时候还可以指定初始大小。 + +```java +List alist = new ArrayList<>(20); +``` + +这样做的好处是,可以有效地避免在添加新的元素时进行不必要的扩容。但通常情况下,我们很难确定 ArrayList 中元素的个数,因此一般不指定初始大小。 + +“二哥,**那怎么向 ArrayList 中添加一个元素呢**?”三妹继续问。 + +可以通过 `add()` 方法向 ArrayList 中添加一个元素,如果不指定下标的话,就默认添加在末尾。 + +```java +alist.add("沉默王二"); +``` + +“三妹,你可以研究一下 `add()` 方法的源码(基于 JDK 8 会好一点),它在添加元素的时候会判断需不需要进行扩容,如果需要的话,会执行 `grow()` 方法进行扩容,这个也是面试官特别喜欢考察的一个重点。”我叮嘱道。 + +下面是 `add(E e)` 方法的源码: + +```java +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; +} +``` + +调用了私有的 `ensureCapacityInternal` 方法: + +```java +private void ensureCapacityInternal(int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + + ensureExplicitCapacity(minCapacity); +} +``` + +假如一开始创建 ArrayList 的时候没有指定大小,elementData 就会被初始化成一个空的数组,也就是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。 + +进入到 if 分支后,minCapacity 的值就会等于 DEFAULT_CAPACITY,可以看一下 DEFAULT_CAPACITY 的初始值: + +```java +private static final int DEFAULT_CAPACITY = 10; +``` +也就是说,如果 ArrayList 在创建的时候没有指定大小,默认可以容纳 10 个元素。 + +接下来会进入 `ensureExplicitCapacity` 方法: + +```java +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + + // overflow-conscious code + if (minCapacity - elementData.length > 0) + grow(minCapacity); +} +``` + +接着进入 `grow(int minCapacity)` 方法: + +```java +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + +然后对数组进行第一次扩容 `Arrays.copyOf(elementData, newCapacity)`,由原来的 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 扩容为容量为 10 的数组。 + +“那假如向 ArrayList 添加第 11 个元素呢?”三妹看到了问题的关键。 + +此时,minCapacity 等于 11,elementData.length 为 10,`ensureExplicitCapacity()` 方法中 if 条件分支就起效了: + +```java +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + + // overflow-conscious code + if (minCapacity - elementData.length > 0) + grow(minCapacity); +} +``` + +会再次进入到 `grow()` 方法: + +```java +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + +“oldCapacity 等于 10,`oldCapacity >> 1` 这个表达式等于多少呢?三妹你知道吗?”我问三妹。 + +“不知道啊,`>>` 是什么意思呢?”三妹很疑惑。 + +“`>>` 是右移运算符,`oldCapacity >> 1` 相当于 oldCapacity 除以 2。”我给三妹解释道,“在计算机内部,都是按照二进制存储的,10 的二进制就是 1010,也就是 `0*2^0 + 1*2^1 + 0*2^2 + 1*2^3`=0+2+0+8=10 。。。。。。” + +还没等我解释完,三妹就打断了我,“二哥,能再详细解释一下到底为什么吗?” + +“当然可以啊。”我拍着胸脯对三妹说。 + +先从位全的含义说起吧。 + +平常我们使用的是十进制数,比如说 39,并不是简单的 3 和 9,3 表示的是 `3*10 = 30`,9 表示的是 `9*1 = 9`,和 3 相乘的 10,和 9 相乘的 1,就是**位权**。位数不同,位权就不同,第 1 位是 10 的 0 次方(也就是 `10^0=1`),第 2 位是 10 的 1 次方(`10^1=10`),第 3 位是 10 的 2 次方(`10^2=100`),最右边的是第一位,依次类推。 + +位权这个概念同样适用于二进制,第 1 位是 2 的 0 次方(也就是 `2^0=1`),第 2 位是 2 的 1 次方(`2^1=2`),第 3 位是 2 的 2 次方(`2^2=4`),第 34 位是 2 的 3 次方(`2^3=8`)。 + +十进制的情况下,10 是基数,二进制的情况下,2 是基数。 + +10 在十进制的表示法是 `0*10^0+1*10^1`=0+10=10。 + +10 的二进制数是 1010,也就是 `0*2^0 + 1*2^1 + 0*2^2 + 1*2^3`=0+2+0+8=10。 + +然后是**移位运算**,移位分为左移和右移,在 Java 中,左移的运算符是 `<<`,右移的运算符 `>>`。 + +拿 `oldCapacity >> 1` 来说吧,`>>` 左边的是被移位的值,此时是 10,也就是二进制 `1010`;`>>` 右边的是要移位的位数,此时是 1。 + +1010 向右移一位就是 101,空出来的最高位此时要补 0,也就是 0101。 + +“那为什么不补 1 呢?”三妹这个问题很尖锐。 + +“因为是算术右移,并且是正数,所以最高位补 0;如果表示的是负数,就需要补 1。”我慢吞吞地回答道,“0101 的十进制就刚好是 `1*2^0 + 0*2^1 + 1*2^2 + 0*2^3`=1+0+4+0=5,如果多移几个数来找规律的话,就会发现,右移 1 位是原来的 1/2,右移 2 位是原来的 1/4,诸如此类。” + +也就是说,ArrayList 的大小会扩容为原来的大小+原来大小/2,也就是差不多 1.5 倍。 + +除了 `add(E e)` 方法,还可以通过 `add(int index, E element)` 方法把元素添加到指定的位置: + +```java +alist.add(0, "沉默王三"); +``` + + `add(int index, E element)` 方法的源码如下: + +```java +public void add(int index, E element) { + rangeCheckForAdd(index); + + ensureCapacityInternal(size + 1); // Increments modCount!! + System.arraycopy(elementData, index, elementData, index + 1, + size - index); + elementData[index] = element; + size++; +} +``` + +该方法会调用到一个非常重要的本地方法 `System.arraycopy()`,它会对数组进行复制(要插入位置上的元素往后复制)。 + +“三妹,注意看,我画幅图来表示下。”我认真地做起了图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraylist-01.png) + + +“二哥,那怎么**更新 ArrayList 中的元素**呢?”三妹继续问。 + +可以使用 `set()` 方法来更改 ArrayList 中的元素,需要提供下标和新元素。 + +```java +alist.set(0, "沉默王四"); +``` + +假设原来 0 位置上的元素为“沉默王三”,现在可以将其更新为“沉默王四”。 + +来看一下 `set()` 方法的源码: + +```java +public E set(int index, E element) { + rangeCheck(index); + + E oldValue = elementData(index); + elementData[index] = element; + return oldValue; +} +``` + +该方法会先对指定的下标进行检查,看是否越界,然后替换新值并返回旧值。 + +“二哥,那怎么**删除 ArrayList 中的元素**呢?”三妹继续问。 + +`remove(int index)` 方法用于删除指定下标位置上的元素,`remove(Object o)` 方法用于删除指定值的元素。 + +```java +alist.remove(1); +alist.remove("沉默王四"); +``` + +先来看 `remove(int index)` 方法的源码: + +```java +public E remove(int index) { + rangeCheck(index); + + modCount++; + E oldValue = elementData(index); + + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // clear to let GC do its work + + return oldValue; +} +``` + +该方法会调用 ` System.arraycopy()` 对数组进行复制移动,然后把要删除的元素位置清空 `elementData[--size] = null`。 + +再来看 `remove(Object o)` 方法的源码: + +```java +public boolean remove(Object o) { + if (o == null) { + for (int index = 0; index < size; index++) + if (elementData[index] == null) { + fastRemove(index); + return true; + } + } else { + for (int index = 0; index < size; index++) + if (o.equals(elementData[index])) { + fastRemove(index); + return true; + } + } + return false; +} +``` + +该方法通过遍历的方式找到要删除的元素,null 的时候使用 == 操作符判断,非 null 的时候使用 `equals()` 方法,然后调用 `fastRemove()` 方法;有相同元素时,只会删除第一个。 + +既然都调用了 `fastRemove()` 方法,那就继续来跟踪一下源码: + +```java +private void fastRemove(int index) { + modCount++; + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // clear to let GC do its work +} +``` + +同样是调用 `System.arraycopy()` 方法对数组进行复制和移动。 + +“三妹,注意看,我画幅图来表示下。”我认真地做起了图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraylist-02.png) + + + +“二哥,那怎么**查找 ArrayList 中的元素**呢?”三妹继续问。 + +如果要正序查找一个元素,可以使用 `indexOf()` 方法;如果要倒序查找一个元素,可以使用 `lastIndexOf()` 方法。 + +```java +alist.indexOf("沉默王二"); +alist.lastIndexOf("沉默王二"); +``` + +来看一下 `indexOf()` 方法的源码: + +```java +public int indexOf(Object o) { + if (o == null) { + for (int i = 0; i < size; i++) + if (elementData[i]==null) + return i; + } else { + for (int i = 0; i < size; i++) + if (o.equals(elementData[i])) + return i; + } + return -1; +} +``` + +如果元素为 null 的时候使用“==”操作符,否则使用 `equals()` 方法。 + +`lastIndexOf()` 方法和 `indexOf()` 方法类似,不过遍历的时候从最后开始。 + +`contains()` 方法可以判断 ArrayList 中是否包含某个元素,其内部调用了 `indexOf()` 方法: + +```java +public boolean contains(Object o) { + return indexOf(o) >= 0; +} +``` + +如果 ArrayList 中的元素是经过排序的,就可以使用二分查找法,效率更快。 + +`Collections` 类的 `sort()` 方法可以对 ArrayList 进行排序,该方法会按照字母顺序对 String 类型的列表进行排序。如果是自定义类型的列表,还可以指定 Comparator 进行排序。 + +```java +List copy = new ArrayList<>(alist); +copy.add("a"); +copy.add("c"); +copy.add("b"); +copy.add("d"); + +Collections.sort(copy); +System.out.println(copy); +``` + +输出结果如下所示: + +``` +[a, b, c, d] +``` + +排序后就可以使用二分查找法了: + +```java +int index = Collections.binarySearch(copy, "b"); +``` + +“最后,三妹,我来简单总结一下 ArrayList 的时间复杂度吧,方便后面学习 LinkedList 时对比。”我喝了一口水后补充道。 + +1)通过下标(也就是 `get(int index)`)访问一个元素的时间复杂度为 O(1),因为是直达的,无论数据增大多少倍,耗时都不变。 + +```java +public E get(int index) { + rangeCheck(index); + + return elementData(index); +} +``` + +2)默认添加一个元素(调用 `add()` 方法时)的时间复杂度为 O(1),因为是直接添加到数组末尾的,但需要考虑到数组扩容时消耗的时间。 + +3)删除一个元素(调用 `remove(Object)` 方法时)的时间复杂度为 O(n),因为要遍历列表,数据量增大几倍,耗时也增大几倍;如果是通过下标删除元素时,要考虑到数组的移动和复制所消耗的时间。 + +4)查找一个未排序的列表时间复杂度为 O(n)(调用 `indexOf()` 或者 `lastIndexOf()` 方法时),因为要遍历列表;查找排序过的列表时间复杂度为 O(log n),因为可以使用二分查找法,当数据增大 n 倍时,耗时增大 logn 倍(这里的 log 是以 2 为底的,每找一次排除一半的可能)。 + +------- + +ArrayList,如果有个中文名的话,应该叫动态数组,也就是可增长的数组,可调整大小的数组。动态数组克服了静态数组的限制,静态数组的容量是固定的,只能在首次创建的时候指定。而动态数组会随着元素的增加自动调整大小,更符合实际的开发需求。 + +学习集合框架,ArrayList 是第一课,也是新手进阶的重要一课。要想完全掌握 ArrayList,扩容这个机制是必须得掌握,也是面试中经常考察的一个点。 + +要想掌握扩容机制,就必须得读源码,也就肯定会遇到 `oldCapacity >> 1`,有些初学者会选择跳过,虽然不影响整体上的学习,但也错过了一个精进的机会。 + +计算机内部是如何表示十进制数的,右移时又发生了什么,静下心来去研究一下,你就会发现,哦,原来这么有趣呢? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/fail-fast.md b/docs/collection/fail-fast.md new file mode 100644 index 0000000000..d79a5d99af --- /dev/null +++ b/docs/collection/fail-fast.md @@ -0,0 +1,248 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 为什么阿里巴巴强制不要在foreach里执行删除操作 + + +那天,小二去阿里面试,面试官老王一上来就甩给了他一道面试题:为什么阿里的 Java 开发手册里会强制不要在 foreach 里进行元素的删除操作? + +----- + +为了镇楼,先搬一段英文来解释一下 fail-fast。 + +>In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them. + +这段话的大致意思就是,fail-fast 是一种通用的系统设计思想,一旦检测到可能会发生错误,就立马抛出异常,程序将不再往下执行。 + +```java +public void test(Wanger wanger) { + if (wanger == null) { + throw new RuntimeException("wanger 不能为空"); + } + + System.out.println(wanger.toString()); +} +``` + +一旦检测到 wanger 为 null,就立马抛出异常,让调用者来决定这种情况下该怎么处理,下一步 `wanger.toString()` 就不会执行了——避免更严重的错误出现。 + +很多时候,我们会把 fail-fast 归类为 Java 集合框架的一种错误检测机制,但其实 fail-fast 并不是 Java 集合框架特有的机制。 + +之所以我们把 fail-fast 放在集合框架篇里介绍,是因为问题比较容易再现。 + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); + +for (String str : list) { + if ("沉默王二".equals(str)) { + list.remove(str); + } +} + +System.out.println(list); +``` + +这段代码看起来没有任何问题,但运行起来就报错了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/fail-fast-01.png) + + +根据错误的堆栈信息,我们可以定位到 ArrayList 的第 901 行代码。 + +```java +final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); +} +``` + +也就是说,remove 的时候触发执行了 `checkForComodification` 方法,该方法对 modCount 和 expectedModCount 进行了比较,发现两者不等,就抛出了 `ConcurrentModificationException` 异常。 + +为什么会执行 `checkForComodification` 方法呢? + +是因为 for-each 本质上是个语法糖,底层是通过[迭代器 Iterator](戳链接🔗,详细了解下) 配合 while 循环实现的,来看一下反编译后的字节码。 + +```java +List list = new ArrayList(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); +Iterator var2 = list.iterator(); + +while(var2.hasNext()) { + String str = (String)var2.next(); + if ("沉默王二".equals(str)) { + list.remove(str); + } +} + +System.out.println(list); +``` + +来看一下 ArrayList 的 iterator 方法吧: + +```java +public Iterator iterator() { + return new Itr(); +} +``` + +内部类 Itr 实现了 Iterator 接口。 + +```java +private class Itr implements Iterator { + int cursor; // index of next element to return + int lastRet = -1; // index of last element returned; -1 if no such + int expectedModCount = modCount; + + Itr() {} + + public boolean hasNext() { + return cursor != size; + } + + @SuppressWarnings("unchecked") + public E next() { + checkForComodification(); + int i = cursor; + Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) + throw new ConcurrentModificationException(); + cursor = i + 1; + return (E) elementData[lastRet = i]; + } +} +``` + +也就是说 `new Itr()` 的时候 expectedModCount 被赋值为 modCount,而 modCount 是 List 的一个成员变量,表示集合被修改的次数。由于 list 此前执行了 3 次 add 方法。 + +- add 方法调用 ensureCapacityInternal 方法 +- ensureCapacityInternal 方法调用 ensureExplicitCapacity 方法 +- ensureExplicitCapacity 方法中会执行 `modCount++` + +所以 modCount 的值在经过三次 add 后为 3,于是 `new Itr()` 后 expectedModCount 的值也为 3。 + +执行第一次循环时,发现“沉默王二”等于 str,于是执行 `list.remove(str)`。 + +- remove 方法调用 fastRemove 方法 +- fastRemove 方法中会执行 `modCount++` + + +```java +private void fastRemove(int index) { + modCount++; + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // clear to let GC do its work +} +``` + +modCount 的值变成了 4。 + +执行第二次循环时,会执行 Itr 的 next 方法(`String str = (String) var3.next();`),next 方法就会调用 `checkForComodification` 方法,此时 expectedModCount 为 3,modCount 为 4,就只好抛出 ConcurrentModificationException 异常了。 + +那其实在阿里巴巴的 Java 开发手册里也提到了,不要在 for-each 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/fail-fast-02.png) + +那原因其实就是我们上面分析的这些,出于 fail-fast 保护机制。 + +**那该如何正确地删除元素呢**? + +**1)remove 后 break** + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); + +for (String str : list) { + if ("沉默王二".equals(str)) { + list.remove(str); + break; + } +} +``` + +break 后循环就不再遍历了,意味着 Iterator 的 next 方法不再执行了,也就意味着 `checkForComodification` 方法不再执行了,所以异常也就不会抛出了。 + +但是呢,当 List 中有重复元素要删除的时候,break 就不合适了。 + + +**2)for 循环** + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); +for (int i = 0, n = list.size(); i < n; i++) { + String str = list.get(i); + if ("沉默王二".equals(str)) { + list.remove(str); + } +} +``` + +for 循环虽然可以避开 fail-fast 保护机制,也就说 remove 元素后不再抛出异常;但是呢,这段程序在原则上是有问题的。为什么呢? + +第一次循环的时候,i 为 0,`list.size()` 为 3,当执行完 remove 方法后,i 为 1,`list.size()` 却变成了 2,因为 list 的大小在 remove 后发生了变化,也就意味着“沉默王三”这个元素被跳过了。能明白吗? + +remove 之前 `list.get(1)` 为“沉默王三”;但 remove 之后 `list.get(1)` 变成了“一个文章真特么有趣的程序员”,而 `list.get(0)` 变成了“沉默王三”。 + +**3)使用 Iterator** + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); + +Iterator itr = list.iterator(); + +while (itr.hasNext()) { + String str = itr.next(); + if ("沉默王二".equals(str)) { + itr.remove(); + } +} +``` + +为什么使用 Iterator 的 remove 方法就可以避开 fail-fast 保护机制呢?看一下 remove 的源码就明白了。 + +```java +public void remove() { + if (lastRet < 0) + throw new IllegalStateException(); + checkForComodification(); + + try { + ArrayList.this.remove(lastRet); + cursor = lastRet; + lastRet = -1; + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } +} +``` + +删除完会执行 `expectedModCount = modCount`,保证了 expectedModCount 与 modCount 的同步。 + +----- + +简单地总结一下,fail-fast 是一种保护机制,可以通过 for-each 循环删除集合的元素的方式验证这种保护机制。 + +那也就是说,for-each 本质上是一种语法糖,遍历集合时很方面,但并不适合拿来操作集合中的元素(增删)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/gailan.md b/docs/collection/gailan.md new file mode 100644 index 0000000000..3d87535199 --- /dev/null +++ b/docs/collection/gailan.md @@ -0,0 +1,296 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java集合框架 + + +眼瞅着三妹的王者荣耀杀得正嗨,我趁机喊到:“别打了,三妹,我们来一起学习 Java 的集合框架吧。” + +“才不要呢,等我打完这一局啊。”三妹倔强地说。 + +“好吧。”我只好摊摊手地说,“那我先画张集合框架的结构图等着你。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/gailan-01.png) + + +“完了没?三妹。” + +“完了好一会儿了,二哥,你图画得真慢,让我瞧瞧怎么样?” + +“害,图要画得清晰明了,不容易的。三妹,你瞧,不错吧。” + +Java 集合框架可以分为两条大的支线: + +- Collection,主要由 List、Set、Queue 组成,List 代表有序、可重复的集合,典型代表就是封装了动态数组的 ArrayList 和封装了链表的 LinkedList;Set 代表无序、不可重复的集合,典型代表就是 HashSet 和 TreeSet;Queue 代表队列,典型代表就是双端队列 ArrayDeque,以及优先级队列 PriorityQue。 +- Map,代表键值对的集合,典型代表就是 HashMap。 + +“接下来,我们再来过一遍。” + +### 01、List + +>List 的特点是存取有序,可以存放重复的元素,可以用下标对元素进行操作 + +**1)ArrayList** + +- ArrayList 是由数组实现的,支持随机存取,也就是可以通过下标直接存取元素; +- 从尾部插入和删除元素会比较快捷,从中间插入和删除元素会比较低效,因为涉及到数组元素的复制和移动; +- 如果内部数组的容量不足时会自动扩容,因此当元素非常庞大的时候,效率会比较低。 + +**2)LinkedList** + +- LinkedList 是由双向链表实现的,不支持随机存取,只能从一端开始遍历,直到找到需要的元素后返回; +- 任意位置插入和删除元素都很方便,因为只需要改变前一个节点和后一个节点的引用即可,不像 ArrayList 那样需要复制和移动数组元素; +- 因为每个元素都存储了前一个和后一个节点的引用,所以相对来说,占用的内存空间会比 ArrayList 多一些。 + +**3)Vector 和 Stack** + +List 的实现类还有一个 Vector,是一个元老级的类,比 ArrayList 出现得更早。ArrayList 和 Vector 非常相似,只不过 Vector 是线程安全的,像 get、set、add 这些方法都加了 `synchronized` 关键字,就导致执行执行效率会比较低,所以现在已经很少用了。 + +更好的选择是并发包下的 CopyOnWriteArrayList。 + +Stack 是 Vector 的一个子类,本质上也是由动态数组实现的,只不过还实现了先进后出的功能(在 get、set、add 方法的基础上追加了 pop、peek 等方法),所以叫栈。 + +不过,由于 Stack 执行效率比较低(方法上同样加了 synchronized 关键字),就被双端队列 ArrayDeque 取代了。 + +### 02、Set + +> Set 的特点是存取无序,不可以存放重复的元素,不可以用下标对元素进行操作,和 List 有很多不同 + +**1)HashSet** + +HashSet 其实是由 HashMap 实现的,只不过值由一个固定的 Object 对象填充,而键用于操作。 + +```java +public class HashSet + extends AbstractSet + implements Set, Cloneable, java.io.Serializable +{ + private transient HashMap map; + + // Dummy value to associate with an Object in the backing Map + private static final Object PRESENT = new Object(); + + public HashSet() { + map = new HashMap<>(); + } + + public boolean add(E e) { + return map.put(e, PRESENT)==null; + } + + public boolean remove(Object o) { + return map.remove(o)==PRESENT; + } +} +``` + +**2)LinkedHashSet** + +LinkedHashSet 继承自 HashSet,其实是由 LinkedHashMap 实现的,LinkedHashSet 的构造方法调用了 HashSet 的一个特殊的构造方法: + +```java +HashSet(int initialCapacity, float loadFactor, boolean dummy) { + map = new LinkedHashMap<>(initialCapacity, loadFactor); +} +``` + +**3)TreeSet** + +“二哥,不用你讲了,我能猜到,TreeSet 是由 TreeMap 实现的,只不过同样操作的键位,值由一个固定的 Object 对象填充。” + +哇,三妹都学会了推理。 + +“是的,总体上来说,Set 集合不是关注的重点,因为底层都是由 Map 实现的,为什么要用 Map 实现呢?三妹你能猜到原因吗?” + +“让我想想。” + +“嗯?难道是因为 Map 的键不允许重复、无序吗?” + +老天,竟然被三妹猜到了。 + +“是的,你这水平长进了呀,三妹。” + +### 03、Queue + +> Queue,也就是队列,通常遵循先进先出(FIFO)的原则,新元素插入到队列的尾部,访问元素返回队列的头部。 + +**1)ArrayDeque** + +从名字上可以看得出,ArrayDeque 是一个基于数组实现的双端队列,为了满足可以同时在数组两端插入或删除元素的需求,数组必须是循环的,也就是说数组的任何一点都可以被看作是起点或者终点。 + +这是一个包含了 4 个元素的双端队列,和一个包含了 5 个元素的双端队列。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/gailan-02.png) + +head 指向队首的第一个有效的元素,tail 指向队尾第一个可以插入元素的空位,因为是循环数组,所以 head 不一定从是从 0 开始,tail 也不一定总是比 head 大。 + +**2)LinkedList** + +LinkedList 一般都归在 List 下,只不过,它也实现了 Deque 接口,可以作为队列来使用。等于说,LinkedList 同时实现了 Stack、Queue、PriorityQueue 的所有功能。 + +**3)PriorityQueue** + +PriorityQueue 是一种优先级队列,它的出队顺序与元素的优先级有关,执行 remove 或者 poll 方法,返回的总是优先级最高的元素。 + +要想有优先级,元素就需要实现 Comparable 接口或者 Comparator 接口。 + +### 04、Map + +> Map 保存的是键值对,键要求保持唯一性,值可以重复。 + +**1)HashMap** + +HashMap 实现了 Map 接口,根据键的 HashCode 值来存储数据,具有很快的访问速度,最多允许一个 null 键。 + +HashMap 不论是在学习还是工作当中,使用频率都是相当高的。随着 JDK 版本的不断更新,HashMap 的底层也优化了很多次,JDK 8 的时候引入了红黑树。 + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + HashMap.Node[] tab; HashMap.Node p; int n, i; + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else { + HashMap.Node e; K k; + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + else if (p instanceof HashMap.TreeNode) + e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + return null; +} +``` + +一旦 HashMap 发生哈希冲突,就把相同键位的地方改成链表,如果链表的长度超过 8,就该用红黑树。 + +**2)LinkedHashMap** + +大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。 + +大多数情况下,只要不涉及到线程安全的问题,有需要键值对的时候就会使用 HashMap,但 HashMap 有一个问题,就是 HashMap 是无序的。在某些场景下,我们需要一个有序的 Map。 + +于是 LinkedHashMap 就闪亮登场了。LinkedHashMap 是 HashMap 的子类,内部使用链表来记录插入/访问元素的顺序。 + +LinkedHashMap 可以看作是 HashMap + LinkedList 的合体,它使用了 哈希表来存储数据,又用了双向链表来维持顺序。 + +**3)TreeMap** + +HashMap 是无序的,所以遍历的时候元素的顺序也是不可测的。TreeMap 是有序的,它在内部会对键进行排序,所以遍历的时候就可以得到预期的顺序。 + +为了保证顺序,TreeMap 的键必须要实现 Comparable 接口或者 Comparator 接口。 + +### 05、时间复杂度 + +“二哥,为什么要讲时间复杂度呀?”三妹问。 + +“因为接下来要用到啊。后面我们学习 ArrayList、LinkedList 的时候,会比较两者在增删改查时的执行效率,而时间复杂度是衡量执行效率的一个重要标准。”我说。 + +“到时候跑一下代码,统计一下前后的时间差不更准确吗?”三妹反问道。 + +“实际上,你说的是另外一种评估方法,这种评估方法可以得出非常准确的数值,但也有很大的局限性。”我不急不慢地说。 + +第一,测试结果会受到测试环境的影响。你比如说,同样的代码,在我这台 iMac 上跑出来的时间和在你那台华为的 MacBook 上抛出的时间可能就差别很大。 + +第二,测试结果会受到测试数据的影响。你比如说,一个排序后的数组和一个没有排序后的数组,调用了同一个查询方法,得出来的结果可能会差别特别大。 + +“因此,我们需要这种不依赖于具体测试环境和测试数据就能粗略地估算出执行效率的方法,时间复杂度就是其中的一种,还有一种是空间复杂度。”我继续补充道。 + +来看下面这段代码: + +```java +public static int sum(int n) { + int sum = 0; // 第 1 行 + for (int i=0;i T(n) = O(f(n)) + +f(n) 表示代码总的执行次数,大写 O 表示代码的执行时间 T(n) 和 f(n) 成正比。 + +这也就是大 O 表示法,它不关心代码具体的执行时间是多少,它关心的是代码执行时间的变化趋势,这也就是时间复杂度这个概念的由来。 + +对于上面那段代码 `sum()` 来说,影响时间复杂度的主要是第 2 行代码,其余的,像系数 2、常数 2 都是可以忽略不计的,我们只关心影响最大的那个,所以时间复杂度就表示为 `O(n)`。 + +常见的时间复杂度有这么 3 个: + +1)`O(1)` + +代码的执行时间,和数据规模 n 没有多大关系。 + +括号中的 1 可以是 3,可以是 5,可以 100,我们习惯用 1 来表示,表示这段代码的执行时间是一个常数级别。比如说下面这段代码: + +```java +int i = 0; +int j = 0; +int k = i + j; +``` + +实际上执行了 3 次,但我们也认为这段代码的时间复杂度为 `O(1)`。 + +2)`O(n)` + +时间复杂度和数据规模 n 是线性关系。换句话说,数据规模增大 K 倍,代码执行的时间就大致增加 K 倍。 + +3)`O(logn)` + +时间复杂度和数据规模 n 是对数关系。换句话说,数据规模大幅增加时,代码执行的时间只有少量增加。 + +来看一下代码示例, + +```java +public static void logn(int n) { + int i = 1; + while (i < n) { + i *= 2; + } +} +``` + +换句话说,当数据量 n 从 2 增加到 2^64 时,代码执行的时间只增加 64 倍。 + +``` +遍历次数 | i +----------+------- + 0 | i + 1 | i*2 + 2 | i*4 + ... | ... + ... | ... + k | i*2^k +``` + +“好了,三妹,这节就讲到这吧,理解了上面 3 个时间复杂度,后面我们学习 ArrayList、LinkedList 的时候,两者在增删改查时的执行效率就很容易对比清楚了。”我伸了个懒腰后对三妹说,“整体上,集合框架就这么多东西了,随后我们会一一展开来讲,比如说 ArrayList、LinkedList、HashMap 等。”。 + +“好的,二哥。”三妹重新回答沙发上,一盘王者荣耀即将开始。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/hashmap-interview.md b/docs/collection/hashmap-interview.md new file mode 100644 index 0000000000..8a68bc6ba9 --- /dev/null +++ b/docs/collection/hashmap-interview.md @@ -0,0 +1,201 @@ +--- +category: + - 求职面试 +tag: + - 面试题集合 +--- + +# Java HashMap精选面试题 + + +对于 Java 求职者来说,HashMap 可谓是重中之重,是面试的必考点。然而 HashMap 的知识点非常多,复习起来花费精力很大。 + + + +### 01、HashMap的底层数据结构是什么? + +JDK 7 中,HashMap 由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。 + +在 JDK 8 中,HashMap 由“数组+链表+红黑树”组成。链表过长,会严重影响 HashMap 的性能,而红黑树搜索的时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK 8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换: + +- 当链表超过 8 且数据总量超过 64 时会转红黑树。 +- 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。 + +链表长度超过 8 体现在 putVal 方法中的这段代码: + +```java +//链表长度大于8转换为红黑树进行处理 +if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); +``` + + table 长度为 64 体现在 treeifyBin 方法中的这段代码:: + +```java +final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + resize(); +} +``` + +MIN_TREEIFY_CAPACITY 的值正好为 64。 + +```java +static final int MIN_TREEIFY_CAPACITY = 64; +``` + +JDK 8 中 HashMap 的结构示意图: + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-interview-01.png) + +### 02、为什么链表改为红黑树的阈值是 8? + +因为泊松分布,我们来看作者在源码中的注释: + +>Because TreeNodes are about twice the size of regular nodes, we + use them only when bins contain enough nodes to warrant use + (see TREEIFY_THRESHOLD). And when they become too small (due to + removal or resizing) they are converted back to plain bins. In + usages with well-distributed user hashCodes, tree bins are + rarely used. Ideally, under random hashCodes, the frequency of + nodes in bins follows a Poisson distribution + (http://en.wikipedia.org/wiki/Poisson_distribution) with a + parameter of about 0.5 on average for the default resizing + threshold of 0.75, although with a large variance because of + resizing granularity. Ignoring variance, the expected + occurrences of list size k are (exp(-0.5) pow(0.5, k) / + factorial(k)). The first values are: + 0: 0.60653066
+ 1: 0.30326533
+ 2: 0.07581633
+ 3: 0.01263606
+ 4: 0.00157952
+ 5: 0.00015795
+ 6: 0.00001316
+ 7: 0.00000094
+ 8: 0.00000006
+ more: less than 1 in ten million + +翻译过来大概的意思是:理想情况下使用随机的哈希码,容器中节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。 + +### 03、解决hash冲突的办法有哪些?HashMap用的哪种? + +解决Hash冲突方法有: + +- 开放定址法:也称为再散列法,基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。 +- 再哈希法:双重散列,多重散列,提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增加了计算的时间。 +- 链地址法:拉链法,将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。 +- 建立公共溢出区:将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。 + +HashMap中采用的是链地址法 。 + +### 04、为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树? + +因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。 + +当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。 + +因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。 + +### 05、HashMap默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ? + +作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。 + +[详情参照这篇](https://mp.weixin.qq.com/s/a3qfatEWizKK1CpYaxVBbA) + +### 06、HashMap 中 key 的存储索引是怎么计算的? + +首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置。 + + +[详情参照这篇](https://mp.weixin.qq.com/s/aS2dg4Dj1Efwujmv-6YTBg) + +### 07、JDK 8 为什么要 hashcode 异或其右移十六位的值? + +因为在JDK 7 中扰动了 4 次,计算 hash 值的性能会稍差一点点。 + +从速度、功效、质量来考虑,JDK 8 优化了高位运算的算法,通过hashCode()的高16位异或低16位实现:`(h = k.hashCode()) ^ (h >>> 16)`。 + +这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。 + +### 08、为什么 hash 值要与length-1相与? + +- 把 hash 值对数组长度取模运算,模运算的消耗很大,没有位运算快。 +- 当 length 总是 2 的n次方时,`h& (length-1) `运算等价于对length取模,也就是 h%length,但是 & 比 % 具有更高的效率。 + +### 09、HashMap数组的长度为什么是 2 的幂次方? + +2 的 N 次幂有助于减少碰撞的几率。如果 length 为2的幂次方,则 length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费。我们来举个例子,看下图: + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-interview-02.png) + +当 length =15时,6 和 7 的结果一样,这样表示他们在 table 存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,4和5的结果也是一样,这样就会导致查询速度降低。 + +如果我们进一步分析,还会发现空间浪费非常大,以 length=15 为例,在 1、3、5、7、9、11、13、15 这八处没有存放数据。因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。 + +**再补充数组容量计算的小奥秘。** + +HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。会取大于或等于这个数的 且最近的2次幂作为 table 数组的初始容量,使用tableSizeFor(int)方法,如 tableSizeFor(10) = 16(2 的 4 次幂),tableSizeFor(20) = 32(2 的 5 次幂),也就是说 table 数组的长度总是 2 的次幂。JDK 8 源码如下: + +```java +static final int tableSizeFor(int cap) { + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } +``` + +让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。 + +### 10、HashMap 的put方法流程? + +以JDK 8为例,简要流程如下: + +1、首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标; + +2、如果数组是空的,则调用 resize 进行初始化; + +3、如果没有哈希冲突直接放在对应的数组下标里; + +4、如果冲突了,且 key 已经存在,就覆盖掉 value; + +5、如果冲突后,发现该节点是红黑树,就将这个节点挂在树上; + +6、如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-interview-03.png) + +### 11、HashMap 的扩容方式? + +HashMap 在容量超过负载因子所定义的容量之后,就会扩容。 + +[详情参照这篇](https://mp.weixin.qq.com/s/0KSpdBJMfXSVH63XadVdmw) + +### 12、一般用什么作为HashMap的key? + +一般用Integer、String 这种不可变类当作 HashMap 的 key,String 最为常见。 + +- 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。 +- 因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的。Integer、String 这些类已经很规范的重写了 hashCode() 以及 equals() 方法。 + +### 13、HashMap为什么线程不安全? + +- JDK 7 时多线程下扩容会造成死循环。 +- 多线程的put可能导致元素的丢失。 +- put和get并发时,可能导致get为null。 + +[详情参照这篇](https://mp.weixin.qq.com/s/qk_neCdzM3aB6pVWVTHhNw) + + + +>参考链接:https://zhuanlan.zhihu.com/p/362214327 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/collection/hashmap.md b/docs/collection/hashmap.md new file mode 100644 index 0000000000..78426ecf76 --- /dev/null +++ b/docs/collection/hashmap.md @@ -0,0 +1,800 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java8系列之重新认识HashMap + +## 一、hash 方法的原理 + +来看一下 hash 方法的源码(JDK 8 中的 HashMap): + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +这段代码究竟是用来干嘛的呢? + +我们都知道,`key.hashCode()` 是用来获取键位的哈希值的,理论上,哈希值是一个 int 类型,范围从-2147483648 到 2147483648。前后加起来大概 40 亿的映射空间,只要哈希值映射得比较均匀松散,一般是不会出现哈希碰撞的。 + +但问题是一个 40 亿长度的数组,内存是放不下的。HashMap 扩容之前的数组初始大小只有 16,所以这个哈希值是不能直接拿来用的,用之前要和数组的长度做取模运算,用得到的余数来访问数组下标才行。 + +取模运算有两处。 + +> 取模运算(“Modulo Operation”)和取余运算(“Remainder Operation ”)两个概念有重叠的部分但又不完全一致。主要的区别在于对负整数进行除法运算时操作不同。取模主要是用于计算机术语中,取余则更多是数学概念。 + +一处是往 HashMap 中 put 的时候(`putVal` 方法中): + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { + HashMap.Node[] tab; HashMap.Node p; int n, i; + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); +} +``` + +一处是从 HashMap 中 get 的时候(`getNode` 方法中): + +```java +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) {} +} +``` + +其中的 `(n - 1) & hash` 正是取模运算,就是把哈希值和(数组长度-1)做了一个“与”运算。 + +可能大家在疑惑:**取模运算难道不该用 `%` 吗?为什么要用 `&` 呢**? + +这是因为 `&` 运算比 `%` 更加高效,并且当 b 为 2 的 n 次方时,存在下面这样一个公式。 + +> a % b = a & (b-1) + +用 $2^n$ 替换下 b 就是: + +>a % $2^n$ = a & ($2^n$-1) + +我们来验证一下,假如 a = 14,b = 8,也就是 $2^3$,n=3。 + +14%8,14 的二进制为 1110,8 的二进制 1000,8-1 = 7 的二进制为 0111,1110&0111=0110,也就是 0`*`$2^0$+1`*`$2^1$+1`*`$2^2$+0`*`$2^3$=0+2+4+0=6,14%8 刚好也等于 6。 + +这也正好解释了为什么 HashMap 的数组长度要取 2 的整次方。 + +因为(数组长度-1)正好相当于一个“低位掩码”——这个掩码的低位最好全是 1,这样 & 操作才有意义,否则结果就肯定是 0,那么 & 操作就没有意义了。 + +> a&b 操作的结果是:a、b 中对应位同时为 1,则对应结果位为 1,否则为 0 + +2 的整次幂刚好是偶数,偶数-1 是奇数,奇数的二进制最后一位是 1,保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 h 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证哈希值的均匀性。 + +& 操作的结果就是将哈希值的高位全部归零,只保留低位值,用来做数组下标访问。 + +假设某哈希值为 `10100101 11000100 00100101`,用它来做取模运算,我们来看一下结果。HashMap 的初始长度为 16(内部是数组),16-1=15,二进制是 `00000000 00000000 00001111`(高位用 0 来补齐): + +``` + 10100101 11000100 00100101 +& 00000000 00000000 00001111 +---------------------------------- + 00000000 00000000 00000101 +``` + +因为 15 的高位全部是 0,所以 & 运算后的高位结果肯定是 0,只剩下 4 个低位 `0101`,也就是十进制的 5,也就是将哈希值为 `10100101 11000100 00100101` 的键放在数组的第 5 位。 + +明白了取模运算后,我们再来看 put 方法的源码: + +```java +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} +``` + +以及 get 方法的源码: + +```java +public V get(Object key) { + HashMap.Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} +``` + +它们在调用 putVal 和 getNode 之前,都会先调用 hash 方法: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +那为什么取模运算之前要调用 hash 方法呢? + +看下面这个图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hash-01.png) + +某哈希值为 `11111111 11111111 11110000 1110 1010`,将它右移 16 位(h >>> 16),刚好是 `00000000 00000000 11111111 11111111`,再进行异或操作(h ^ (h >>> 16)),结果是 `11111111 11111111 00001111 00010101` + +> 异或(`^`)运算是基于二进制的位运算,采用符号 XOR 或者`^`来表示,运算规则是:如果是同值取 0、异值取 1 + +由于混合了原来哈希值的高位和低位,所以低位的随机性加大了(掺杂了部分高位的特征,高位的信息也得到了保留)。 + +结果再与数组长度-1(`00000000 00000000 00000000 00001111`)做取模运算,得到的下标就是 `00000000 00000000 00000000 00000101`,也就是 5。 + +还记得之前我们假设的某哈希值 `10100101 11000100 00100101` 吗?在没有调用 hash 方法之前,与 15 做取模运算后的结果也是 5,我们不妨来看看调用 hash 之后的取模运算结果是多少。 + +某哈希值 `00000000 10100101 11000100 00100101`(补齐 32 位),将它右移 16 位(h >>> 16),刚好是 `00000000 00000000 00000000 10100101`,再进行异或操作(h ^ (h >>> 16)),结果是 `00000000 10100101 00111011 10000000` + +结果再与数组长度-1(`00000000 00000000 00000000 00001111`)做取模运算,得到的下标就是 `00000000 00000000 00000000 00000000`,也就是 0。 + +综上所述,hash 方法是用来做哈希值优化的,把哈希值右移 16 位,也就正好是自己长度的一半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。 + +说白了,**hash 方法就是为了增加随机性,让数据元素更加均衡的分布,减少碰撞**。 + +## 二、扩容机制 + +大家都知道,数组一旦初始化后大小就无法改变了,所以就有了 [ArrayList](https://mp.weixin.qq.com/s/7puyi1PSbkFEIAz5zbNKxA)这种“动态数组”,可以自动扩容。 + +HashMap 的底层用的也是数组。向 HashMap 里不停地添加元素,当数组无法装载更多元素时,就需要对数组进行扩容,以便装入更多的元素。 + +当然了,数组是无法自动扩容的,所以如果要扩容的话,就需要新建一个大的数组,然后把小数组的元素复制过去。 + +HashMap 的扩容是通过 resize 方法来实现的,JDK 8 中融入了红黑树,比较复杂,为了便于理解,就还使用 JDK 7 的源码,搞清楚了 JDK 7 的,我们后面再详细说明 JDK 8 和 JDK 7 之间的区别。 + +resize 方法的源码: + +```java +// newCapacity为新的容量 +void resize(int newCapacity) { + // 小数组,临时过度下 + Entry[] oldTable = table; + // 扩容前的容量 + int oldCapacity = oldTable.length; + // MAXIMUM_CAPACITY 为最大容量,2 的 30 次方 = 1<<30 + if (oldCapacity == MAXIMUM_CAPACITY) { + // 容量调整为 Integer 的最大值 0x7fffffff(十六进制)=2 的 31 次方-1 + threshold = Integer.MAX_VALUE; + return; + } + + // 初始化一个新的数组(大容量) + Entry[] newTable = new Entry[newCapacity]; + // 把小数组的元素转移到大数组中 + transfer(newTable, initHashSeedAsNeeded(newCapacity)); + // 引用新的大数组 + table = newTable; + // 重新计算阈值 + threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); +} +``` + +代码注释里出现了左移(`<<`),这里简单介绍一下: + +``` +a=39 +b = a << 2 +``` + +十进制 39 用 8 位的二进制来表示,就是 00100111,左移两位后是 10011100(低位用 0 补上),再转成十进制数就是 156。 + +移位运算通常可以用来代替乘法运算和除法运算。例如,将 0010011(39)左移两位就是 10011100(156),刚好变成了原来的 4 倍。 + +实际上呢,二进制数左移后会变成原来的 2 倍、4 倍、8 倍。 + +transfer 方法用来转移,将小数组的元素拷贝到新的数组中。 + +```java +void transfer(Entry[] newTable, boolean rehash) { + // 新的容量 + int newCapacity = newTable.length; + // 遍历小数组 + for (Entry e : table) { + while(null != e) { + // 拉链法,相同 key 上的不同值 + Entry next = e.next; + // 是否需要重新计算 hash + if (rehash) { + e.hash = null == e.key ? 0 : hash(e.key); + } + // 根据大数组的容量,和键的 hash 计算元素在数组中的下标 + int i = indexFor(e.hash, newCapacity); + + // 同一位置上的新元素被放在链表的头部 + e.next = newTable[i]; + + // 放在新的数组上 + newTable[i] = e; + + // 链表上的下一个元素 + e = next; + } + } +} +``` + +`e.next = newTable[i]`,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到链表的尾部(如果发生了hash冲突的话),这一点和 JDK 8 有区别。 + +**在旧数组中同一个链表上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上**(仔细看下面的内容,会解释清楚这一点)。 + +假设 hash 算法([之前的章节有讲到](https://mp.weixin.qq.com/s/aS2dg4Dj1Efwujmv-6YTBg),点击链接再温故一下)就是简单的用键的哈希值(一个 int 值)和数组大小取模(也就是 hashCode % table.length)。 + +继续假设: + +- 数组 table 的长度为 2 +- 键的哈希值为 3、7、5 + +取模运算后,哈希冲突都到 table[1] 上了,因为余数为 1。那么扩容前的样子如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-resize-01.png) + +小数组的容量为 2, key 3、7、5 都在 table[1] 的链表上。 + +假设负载因子 loadFactor 为 1,也就是当元素的实际大小大于 table 的实际大小时进行扩容。 + +扩容后的大数组的容量为 4。 + +- key 3 取模(3%4)后是 3,放在 table[3] 上。 +- key 7 取模(7%4)后是 3,放在 table[3] 上的链表头部。 +- key 5 取模(5%4)后是 1,放在 table[1] 上。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-resize-02.png) + +按照我们的预期,扩容后的 7 仍然应该在 3 这条链表的后面,但实际上呢? 7 跑到 3 这条链表的头部了。针对 JDK 7 中的这个情况,JDK 8 做了哪些优化呢? + +看下面这张图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-resize-03.png) + +n 为 table 的长度,默认值为 16。 + +- n-1 也就是二进制的 0000 1111(1X$2^0$+1X$2^1$+1X$2^2$+1X$2^3$=1+2+4+8=15); +- key1 哈希值的最后 8 位为 0000 0101 +- key2 哈希值的最后 8 位为 0001 0101(和 key1 不同) +- 做与运算后发生了哈希冲突,索引都在(0000 0101)上。 + +扩容后为 32。 + +- n-1 也就是二进制的 0001 1111(1X$2^0$+1X$2^1$+1X$2^2$+1X$2^3$+1X$2^4$=1+2+4+8+16=31),扩容前是 0000 1111。 +- key1 哈希值的低位为 0000 0101 +- key2 哈希值的低位为 0001 0101(和 key1 不同) +- key1 做与运算后,索引为 0000 0101。 +- key2 做与运算后,索引为 0001 0101。 + +新的索引就会发生这样的变化: + +- 原来的索引是 5(*0* 0101) +- 原来的容量是 16 +- 扩容后的容量是 32 +- 扩容后的索引是 21(*1* 0101),也就是 5+16,也就是原来的索引+原来的容量 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-resize-04.png) + + +也就是说,JDK 8 不需要像 JDK 7 那样重新计算 hash,只需要看原来的hash值新增的那个bit是1还是0就好了,是0的话就表示索引没变,是1的话,索引就变成了“原索引+原来的容量”。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-resize-05.png) + +JDK 8 的这个设计非常巧妙,既省去了重新计算hash的时间,同时,由于新增的1 bit是0还是1是随机的,因此扩容的过程,可以均匀地把之前的节点分散到新的位置上。 + + woc,只能说 HashMap 的作者 Doug Lea、Josh Bloch、Arthur van Hoff、Neal Gafter 真的强——的一笔。 + +JDK 8 扩容的源代码: + +```java +final Node[] resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) { + // 超过最大值就不再扩充了,就只好随你碰撞去吧 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + // 没超过最大值,就扩充为原来的2倍 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 计算新的resize上限 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; + if (oldTab != null) { + // 小数组复制到大数组 + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + oldTab[j] = null; + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + else { // preserve order + // 链表优化重 hash 的代码块 + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 原来的索引 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + // 索引+原来的容量 + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} +``` + +## 三、加载因子为什么是0.75 + +JDK 8 中的 HashMap 是用数组+链表+红黑树实现的,我们要想往 HashMap 中放数据或者取数据,就需要确定数据在数组中的下标。 + +先把数据的键进行一次 hash: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +再做一次取模运算确定下标: + +``` +i = (n - 1) & hash +``` + +哈希表这样的数据结构容易产生两个问题: + +- 数组的容量过小,经过哈希计算后的下标,容易出现冲突; +- 数组的容量过大,导致空间利用率不高。 + +加载因子是用来表示 HashMap 中数据的填满程度: + +>加载因子 = 填入哈希表中的数据个数 / 哈希表的长度 + +这就意味着: + +- 加载因子越小,填满的数据就越少,哈希冲突的几率就减少了,但浪费了空间,而且还会提高扩容的触发几率; +- 加载因子越大,填满的数据就越多,空间利用率就高,但哈希冲突的几率就变大了。 + +好难!!!! + +这就必须在“**哈希冲突**”与“**空间利用率**”两者之间有所取舍,尽量保持平衡,谁也不碍着谁。 + +我们知道,HashMap 是通过拉链法来解决哈希冲突的。 + +为了减少哈希冲突发生的概率,当 HashMap 的数组长度达到一个**临界值**的时候,就会触发扩容(可以点击[链接](https://mp.weixin.qq.com/s/0KSpdBJMfXSVH63XadVdmw)查看 HashMap 的扩容机制),扩容后会将之前小数组中的元素转移到大数组中,这是一个相当耗时的操作。 + +这个临界值由什么来确定呢? + +>临界值 = 初始容量 * 加载因子 + +一开始,HashMap 的容量是 16: + +```java +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +``` + +加载因子是 0.75: + +```java +static final float DEFAULT_LOAD_FACTOR = 0.75f; +``` + +也就是说,当 16*0.75=12 时,会触发扩容机制。 + +为什么加载因子会选择 0.75 呢?为什么不是0.8、0.6呢? + +这跟统计学里的一个很重要的原理——泊松分布有关。 + +是时候上维基百科了: + +>泊松分布,是一种统计与概率学里常见到的离散概率分布,由法国数学家西莫恩·德尼·泊松在1838年时提出。它会对随机事件的发生次数进行建模,适用于涉及计算在给定的时间段、距离、面积等范围内发生随机事件的次数的应用情形。 + +阮一峰老师曾在一篇博文中详细的介绍了泊松分布和指数分布,大家可以去看一下。 + +>链接:https://www.ruanyifeng.com/blog/2015/06/poisson-distribution.html + +具体是用这么一个公式来表示的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-01.png) + +等号的左边,P 表示概率,N表示某种函数关系,t 表示时间,n 表示数量。 + +在 HashMap 的 doc 文档里,曾有这么一段描述: + +``` +Because TreeNodes are about twice the size of regular nodes, we +use them only when bins contain enough nodes to warrant use +(see TREEIFY_THRESHOLD). And when they become too small (due to +removal or resizing) they are converted back to plain bins. In +usages with well-distributed user hashCodes, tree bins are +rarely used. Ideally, under random hashCodes, the frequency of +nodes in bins follows a Poisson distribution +(http://en.wikipedia.org/wiki/Poisson_distribution) with a +parameter of about 0.5 on average for the default resizing +threshold of 0.75, although with a large variance because of +resizing granularity. Ignoring variance, the expected +occurrences of list size k are (exp(-0.5) * pow(0.5, k) / +factorial(k)). The first values are: +0: 0.60653066 +1: 0.30326533 +2: 0.07581633 +3: 0.01263606 +4: 0.00157952 +5: 0.00015795 +6: 0.00001316 +7: 0.00000094 +8: 0.00000006 +more: less than 1 in ten million +``` + +大致的意思就是: + +因为 TreeNode(红黑树)的大小约为链表节点的两倍,所以我们只有在一个拉链已经拉了足够节点的时候才会转为tree(参考TREEIFY_THRESHOLD)。并且,当这个hash桶的节点因为移除或者扩容后resize数量变小的时候,我们会将树再转为拉链。如果一个用户的数据的hashcode值分布得很均匀的话,就会很少使用到红黑树。 + +理想情况下,我们使用随机的hashcode值,加载因子为0.75情况,尽管由于粒度调整会产生较大的方差,节点的分布频率仍然会服从参数为0.5的泊松分布。链表的长度为 8 发生的概率仅有 0.00000006。 + +虽然这段话的本意更多的是表示 jdk 8中为什么拉链长度超过8的时候进行了红黑树转换,但提到了 0.75 这个加载因子——但这并不是为什么加载因子是 0.75 的答案。 + +为了搞清楚到底为什么,我看到了这篇文章: + +>参考链接:https://segmentfault.com/a/1190000023308658 + +里面提到了一个概念:**二项分布**(二哥概率论没学好,只能简单说一说)。 + +在做一件事情的时候,其结果的概率只有2种情况,和抛硬币一样,不是正面就是反面。 + +为此,我们做了 N 次实验,那么在每次试验中只有两种可能的结果,并且每次实验是独立的,不同实验之间互不影响,每次实验成功的概率都是一样的。 + +以此理论为基础,我们来做这样的实验:我们往哈希表中扔数据,如果发生哈希冲突就为失败,否则为成功。 + +我们可以设想,实验的hash值是随机的,并且经过hash运算的键都会映射到hash表的地址空间上,那么这个结果也是随机的。所以,每次put的时候就相当于我们在扔一个16面(我们先假设默认长度为16)的骰子,扔骰子实验那肯定是相互独立的。碰撞发生即扔了n次有出现重复数字。 + +然后,我们的目的是啥呢? + +就是掷了k次骰子,没有一次是相同的概率,需要尽可能的大些,一般意义上我们肯定要大于0.5(这个数是个理想数,但是我是能接受的)。 + +于是,n次事件里面,碰撞为0的概率,由上面公式得: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-02.png) + +这个概率值需要大于0.5,我们认为这样的hashmap可以提供很低的碰撞率。所以: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-03png) + +这时候,我们对于该公式其实最想求的时候长度s的时候,n为多少次就应该进行扩容了?而负载因子则是$n/s$的值。所以推导如下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-04.png) + +所以可以得到 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-05.png) + +其中 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-06.png) + +这就是一个求 `∞⋅0`函数极限问题,这里我们先令$s = m+1(m \to \infty)$则转化为 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-07.png) + +我们再令 $x = \frac{1}{m} (x \to 0)$ 则有, + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-08.png) + +所以, + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-loadfactor-09.png) + + +考虑到 HashMap的容量有一个要求:它必须是2的n 次幂(这个[之前的文章](https://mp.weixin.qq.com/s/aS2dg4Dj1Efwujmv-6YTBg)讲过了,点击链接回去可以再温故一下)。当加载因子选择了0.75就可以保证它与容量的乘积为整数。 + +``` +16*0.75=12 +32*0.75=24 +``` + +除了 0.75,0.5~1 之间还有 0.625(5/8)、0.875(7/8)可选,从中位数的角度,挑 0.75 比较完美。另外,维基百科上说,拉链法(解决哈希冲突的一种)的加载因子最好限制在 0.7-0.8以下,超过0.8,查表时的CPU缓存不命中(cache missing)会按照指数曲线上升。 + +综上,0.75 是个比较完美的选择。 + +## 四、线程不安全 + +三方面原因:多线程下扩容会死循环、多线程下 put 会导致元素丢失、put 和 get 并发时会导致 get 到 null,我们来一一分析。 + +### 01、多线程下扩容会死循环 + +众所周知,HashMap 是通过拉链法来解决哈希冲突的,也就是当哈希冲突时,会将相同哈希值的键值对通过链表的形式存放起来。 + +JDK 7 时,采用的是头部插入的方式来存放链表的,也就是下一个冲突的键值对会放在上一个键值对的前面(同一位置上的新元素被放在链表的头部)。扩容的时候就有可能导致出现环形链表,造成死循环。 + +resize 方法的源码: + +```java +// newCapacity为新的容量 +void resize(int newCapacity) { + // 小数组,临时过度下 + Entry[] oldTable = table; + // 扩容前的容量 + int oldCapacity = oldTable.length; + // MAXIMUM_CAPACITY 为最大容量,2 的 30 次方 = 1<<30 + if (oldCapacity == MAXIMUM_CAPACITY) { + // 容量调整为 Integer 的最大值 0x7fffffff(十六进制)=2 的 31 次方-1 + threshold = Integer.MAX_VALUE; + return; + } + + // 初始化一个新的数组(大容量) + Entry[] newTable = new Entry[newCapacity]; + // 把小数组的元素转移到大数组中 + transfer(newTable, initHashSeedAsNeeded(newCapacity)); + // 引用新的大数组 + table = newTable; + // 重新计算阈值 + threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); +} +``` + +transfer 方法用来转移,将小数组的元素拷贝到新的数组中。 + +```java +void transfer(Entry[] newTable, boolean rehash) { + // 新的容量 + int newCapacity = newTable.length; + // 遍历小数组 + for (Entry e : table) { + while(null != e) { + // 拉链法,相同 key 上的不同值 + Entry next = e.next; + // 是否需要重新计算 hash + if (rehash) { + e.hash = null == e.key ? 0 : hash(e.key); + } + // 根据大数组的容量,和键的 hash 计算元素在数组中的下标 + int i = indexFor(e.hash, newCapacity); + + // 同一位置上的新元素被放在链表的头部 + e.next = newTable[i]; + + // 放在新的数组上 + newTable[i] = e; + + // 链表上的下一个元素 + e = next; + } + } +} +``` + +注意 `e.next = newTable[i]` 和 `newTable[i] = e` 这两行代码,就会将同一位置上的新元素被放在链表的头部。 + +扩容前的样子假如是下面这样子。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-01.png) + +那么正常扩容后就是下面这样子。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-02.png) + +假设现在有两个线程同时进行扩容,线程 A 在执行到 `newTable[i] = e;` 被挂起,此时线程 A 中:e=3、next=7、e.next=null + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-03.png) + + +线程 B 开始执行,并且完成了数据转移。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-04.png) + + +此时,7 的 next 为 3,3 的 next 为 null。 + +随后线程A获得CPU时间片继续执行 `newTable[i] = e`,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-05.png) + +执行下一轮循环,此时 e=7,原本线程 A 中 7 的 next 为 5,但由于 table 是线程 A 和线程 B 共享的,而线程 B 顺利执行完后,7 的 next 变成了 3,那么此时线程 A 中,7 的 next 也为 3 了。 + +采用头部插入的方式,变成了下面这样子: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-06.png) + +好像也没什么问题,此时 next = 3,e = 3。 + +进行下一轮循环,但此时,由于线程 B 将 3 的 next 变为了 null,所以此轮循环应该是最后一轮了。 + +接下来当执行完 `e.next=newTable[i]` 即 3.next=7 后,3 和 7 之间就相互链接了,执行完 `newTable[i]=e` 后,3 被头插法重新插入到链表中,执行结果如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-07.png) + +套娃开始,元素 5 也就成了弃婴,惨~~~ + +不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序,参照[HashMap 扩容机制](https://mp.weixin.qq.com/s/0KSpdBJMfXSVH63XadVdmw)的这一篇。 + +### 02、多线程下 put 会导致元素丢失 + +正常情况下,当发生哈希冲突时,HashMap 是这样的: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-08.png) + +但多线程同时执行 put 操作时,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。 + +put 的源码: + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + + // 步骤①:tab为空则创建 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + + // 步骤②:计算index,并对null做处理 + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else { + Node e; K k; + + // 步骤③:节点key存在,直接覆盖value + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + + // 步骤④:判断该链为红黑树 + else if (p instanceof TreeNode) + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + + // 步骤⑤:该链为链表 + else { + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + + //链表长度大于8转换为红黑树进行处理 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + + // key已经存在直接覆盖value + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + + // 步骤⑥、直接覆盖 + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + + // 步骤⑦:超过最大容量 就扩容 + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; +} +``` + +问题发生在步骤 ② 这里: + +```java +if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); +``` + +两个线程都执行了 if 语句,假设线程 A 先执行了 ` tab[i] = newNode(hash, key, value, null)`,那 table 是这样的: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-09.png) + +接着,线程 B 执行了 ` tab[i] = newNode(hash, key, value, null)`,那 table 是这样的: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-10.png) + +3 被干掉了。 + +### 03、put 和 get 并发时会导致 get 到 null + +线程 A 执行put时,因为元素个数超出阈值而出现扩容,线程B 此时执行get,有可能导致这个问题。 + +注意来看 resize 源码: + +```java +final Node[] resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) { + // 超过最大值就不再扩充了,就只好随你碰撞去吧 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + // 没超过最大值,就扩充为原来的2倍 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 计算新的resize上限 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; +} +``` + +线程 A 执行完 `table = newTab` 之后,线程 B 中的 table 此时也发生了变化,此时去 get 的时候当然会 get 到 null 了,因为元素还没有转移。 + +参考链接: + +> - https://blog.csdn.net/lonyw/article/details/80519652 +> - https://zhuanlan.zhihu.com/p/91636401 +> - https://www.zhihu.com/question/20733617 +> - https://zhuanlan.zhihu.com/p/21673805 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/iterator-iterable.md b/docs/collection/iterator-iterable.md new file mode 100644 index 0000000000..9bdcf7f410 --- /dev/null +++ b/docs/collection/iterator-iterable.md @@ -0,0 +1,247 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java中的Iterator和Iterable区别 + + +那天,小二去海康威视面试,面试官老王一上来就甩给了他一道面试题:请问 Iterator与Iterable有什么区别? + +----- + +在 Java 中,我们对 List 进行遍历的时候,主要有这么三种方式。 + +第一种:for 循环。 + +```java +for (int i = 0; i < list.size(); i++) { + System.out.print(list.get(i) + ","); +} +``` + +第二种:迭代器。 + +```java +Iterator it = list.iterator(); +while (it.hasNext()) { + System.out.print(it.next() + ","); +} +``` + +第三种:for-each。 + +```java +for (String str : list) { + System.out.print(str + ","); +} +``` + +第一种我们略过,第二种用的是 Iterator,第三种看起来是 for-each,其实背后也是 Iterator,看一下反编译后的代码就明白了。 + +```java +Iterator var3 = list.iterator(); + +while(var3.hasNext()) { + String str = (String)var3.next(); + System.out.print(str + ","); +} +``` + +for-each 只不过是个语法糖,让我们在遍历 List 的时候代码更简洁明了。 + +Iterator 是个接口,JDK 1.2 的时候就有了,用来改进 Enumeration: + +- 允许删除元素(增加了 remove 方法) +- 优化了方法名(Enumeration 中是 hasMoreElements 和 nextElement,不简洁) + +来看一下 Iterator 的源码: + +```java +public interface Iterator { + // 判断集合中是否存在下一个对象 + boolean hasNext(); + // 返回集合中的下一个对象,并将访问指针移动一位 + E next(); + // 删除集合中调用next()方法返回的对象 + default void remove() { + throw new UnsupportedOperationException("remove"); + } +} +``` + +JDK 1.8 时,Iterable 接口中新增了 forEach 方法: + +```java +default void forEach(Consumer action) { + Objects.requireNonNull(action); + for (T t : this) { + action.accept(t); + } +} +``` + +它对 Iterable 的每个元素执行给定操作,具体指定的操作需要自己写Consumer接口通过accept方法回调出来。 + +```java +List list = new ArrayList<>(Arrays.asList(1, 2, 3)); +list.forEach(integer -> System.out.println(integer)); +``` + +写得更浅显易懂点,就是: + +```java +List list = new ArrayList<>(Arrays.asList(1, 2, 3)); +list.forEach(new Consumer() { + @Override + public void accept(Integer integer) { + System.out.println(integer); + } +}); +``` + +如果我们仔细观察ArrayList 或者 LinkedList 的“户口本”就会发现,并没有直接找到 Iterator 的影子。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/iterator-iterable-01.png) + +反而找到了 Iterable! + +```java +public interface Iterable { + Iterator iterator(); +} +``` + +也就是说,List 的关系图谱中并没有直接使用 Iterator,而是使用 Iterable 做了过渡。 + +回头再来看一下第二种遍历 List 的方式。 + +```java +Iterator it = list.iterator(); +while (it.hasNext()) { +} +``` + +发现刚好呼应上了。拿 ArrayList 来说吧,它重写了 Iterable 接口的 iterator 方法: + +```java +public Iterator iterator() { + return new Itr(); +} +``` + +返回的对象 Itr 是个内部类,实现了 Iterator 接口,并且按照自己的方式重写了 hasNext、next、remove 等方法。 + +```java +private class Itr implements Iterator { + + public boolean hasNext() { + return cursor != size; + } + + @SuppressWarnings("unchecked") + public E next() { + Object[] elementData = ArrayList.this.elementData; + cursor = i + 1; + return (E) elementData[lastRet = i]; + } + + public void remove() { + try { + ArrayList.this.remove(lastRet); + cursor = lastRet; + lastRet = -1; + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } + } + +} +``` + +那可能有些小伙伴会问:为什么不直接将 Iterator 中的核心方法 hasNext、next 放到 Iterable 接口中呢?直接像下面这样使用不是更方便? + +```java +Iterable it = list.iterator(); +while (it.hasNext()) { +} +``` + +从英文单词的后缀语法上来看,(Iterable)able 表示这个 List 是支持迭代的,而 (Iterator)tor 表示这个 List 是如何迭代的。 + +支持迭代与具体怎么迭代显然不能混在一起,否则就乱的一笔。还是各司其职的好。 + +想一下,如果把 Iterator 和 Iterable 合并,for-each 这种遍历 List 的方式是不是就不好办了? + +原则上,只要一个 List 实现了 Iterable 接口,那么它就可以使用 for-each 这种方式来遍历,那具体该怎么遍历,还是要看它自己是怎么实现 Iterator 接口的。 + +Map 就没办法直接使用 for-each,因为 Map 没有实现 Iterable 接口,只有通过 `map.entrySet()`、`map.keySet()`、`map.values()` 这种返回一个 Collection 的方式才能 使用 for-each。 + +如果我们仔细研究 LinkedList 的源码就会发现,LinkedList 并没有直接重写 Iterable 接口的 iterator 方法,而是由它的父类 AbstractSequentialList 来完成。 + +```java +public Iterator iterator() { + return listIterator(); +} +``` + +LinkedList 重写了 listIterator 方法: + +```java +public ListIterator listIterator(int index) { + checkPositionIndex(index); + return new ListItr(index); +} +``` + +这里我们发现了一个新的迭代器 ListIterator,它继承了 Iterator 接口,在遍历List 时可以从任意下标开始遍历,而且支持双向遍历。 + +```java +public interface ListIterator extends Iterator { + boolean hasNext(); + E next(); + boolean hasPrevious(); + E previous(); +} +``` + +我们知道,集合(Collection)不仅有 List,还有 Map 和 Set,那 Iterator 不仅支持 List,还支持 Set,但 ListIterator 就只支持 List。 + +那可能有些小伙伴会问:为什么不直接让 List 实现 Iterator 接口,而是要用内部类来实现呢? + +这是因为有些 List 可能会有多种遍历方式,比如说 LinkedList,除了支持正序的遍历方式,还支持逆序的遍历方式——DescendingIterator: + +```java +private class DescendingIterator implements Iterator { + private final ListItr itr = new ListItr(size()); + public boolean hasNext() { + return itr.hasPrevious(); + } + public E next() { + return itr.previous(); + } + public void remove() { + itr.remove(); + } +} +``` + +可以看得到,DescendingIterator 刚好利用了 ListIterator 向前遍历的方式。可以通过以下的方式来使用: + +```java +Iterator it = list.descendingIterator(); +while (it.hasNext()) { +} +``` +----- + +好了,关于Iterator与Iterable我们就先聊这么多,总结两点: + +- 学会深入思考,一点点抽丝剥茧,多想想为什么这样实现,很多问题没有自己想象中的那么复杂。 +- 遇到疑惑不放弃,这是提升自己最好的机会,遇到某个疑难的点,解决的过程中会挖掘出很多相关的东西。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/linkedlist.md b/docs/collection/linkedlist.md new file mode 100644 index 0000000000..5375b09fad --- /dev/null +++ b/docs/collection/linkedlist.md @@ -0,0 +1,387 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java集合LinkedList详解 + + +### 一、LinkedList 的剖白 + +大家好,我是 LinkedList,和 ArrayList 是同门师兄弟,但我俩练的内功却完全不同。师兄练的是动态数组,我练的是链表。 + +问大家一个问题,知道我为什么要练链表这门内功吗? + +举个例子来讲吧,假如你们手头要管理一推票据,可能有一张,也可能有一亿张。 + +该怎么办呢? + +申请一个 10G 的大数组等着?那万一票据只有 100 张呢? + +申请一个默认大小的数组,随着数据量的增大扩容?要知道扩容是需要重新复制数组的,很耗时间。 + +关键是,数组还有一个弊端就是,假如现在有 500 万张票据,现在要从中间删除一个票据,就需要把 250 万张票据往前移动一格。 + +遇到这种情况的时候,我师兄几乎情绪崩溃,难受的要命。师父不忍心看到师兄这样痛苦,于是打我进入师门那一天,就强迫我练链表这门内功,一开始我很不理解,害怕师父偏心,不把师门最厉害的内功教我。 + +直到有一天,我亲眼目睹师兄差点因为移动数据而走火入魔,我才明白师父的良苦用心。从此以后,我苦练“链表”这门内功,取得了显著的进步,师父和师兄都夸我有天赋。 + +链表这门内功大致分为三个层次: + +- 第一层叫做“单向链表”,我只有一个后指针,指向下一个数据; +- 第二层叫做“双向链表”,我有两个指针,后指针指向下一个数据,前指针指向上一个数据。 +- 第三层叫做“二叉树”,把后指针去掉,换成左右指针。 + +但我现在的功力还达不到第三层,不过师父说我有这个潜力,练成神功是早晚的事。 + +### 二、LinkedList 的内功心法 + +好了,经过我这么样的一个剖白后,大家对我应该已经不陌生了。那么接下来,我给大家展示一下我的内功心法。 + +我的内功心法主要是一个私有的静态内部类,叫 Node,也就是节点。 + +```java +private static class Node { + E item; + Node next; + Node prev; + + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } +} +``` + +它由三部分组成: + +- 节点上的元素 +- 下一个节点 +- 上一个节点 + +我画幅图给你们展示下吧。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/linkedlist-01.png) + +- 对于第一个节点来说,prev 为 null; +- 对于最后一个节点来说,next 为 null; +- 其余的节点呢,prev 指向前一个,next 指向后一个。 + +我的内功心法就这么简单,其实我早已经牢记在心了。但师父叮嘱我,每天早上醒来的时候,每天晚上睡觉的时候,一定要默默地背诵一遍。虽然我有些厌烦,但我对师父的教诲从来都是言听计从。 + +### 03、LinkedList 的招式 + +和师兄 ArrayList 一样,我的招式也无外乎“增删改查”这 4 种。在此之前,我们都必须得初始化。 + +```java +LinkedList list = new LinkedList(); +``` + +师兄在初始化的时候,默认大小为 10,也可以指定大小,依据要存储的元素数量来。我就不需要。 + +**1)招式一:增** + +可以调用 add 方法添加元素: + +```java +list.add("沉默王二"); +list.add("沉默王三"); +list.add("沉默王四"); +``` + +add 方法内部其实调用的是 linkLast 方法: + +```java +public boolean add(E e) { + linkLast(e); + return true; +} +``` + +linkLast,顾名思义,就是在链表的尾部链接: + +```java +void linkLast(E e) { + final Node l = last; + final Node newNode = new Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} +``` + +- 添加第一个元素的时候,first 和 last 都为 null。 +- 然后新建一个节点 newNode,它的 prev 和 next 也为 null。 +- 然后把 last 和 first 都赋值为 newNode。 + +此时还不能称之为链表,因为前后节点都是断裂的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/linkedlist-02.png) + +- 添加第二个元素的时候,first 和 last 都指向的是第一个节点。 +- 然后新建一个节点 newNode,它的 prev 指向的是第一个节点,next 为 null。 +- 然后把第一个节点的 next 赋值为 newNode。 + +此时的链表还不完整。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/linkedlist-03.png) + +- 添加第三个元素的时候,first 指向的是第一个节点,last 指向的是最后一个节点。 +- 然后新建一个节点 newNode,它的 prev 指向的是第二个节点,next 为 null。 +- 然后把第二个节点的 next 赋值为 newNode。 + +此时的链表已经完整了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/linkedlist-04.png) + +我这个增的招式,还可以演化成另外两个: + +- `addFirst()` 方法将元素添加到第一位; +- `addLast()` 方法将元素添加到末尾。 + +addFirst 内部其实调用的是 linkFirst: + +```java +public void addFirst(E e) { + linkFirst(e); +} +``` + +linkFirst 负责把新的节点设为 first,并将新的 first 的 next 更新为之前的 first。 + +```java +private void linkFirst(E e) { + final Node f = first; + final Node newNode = new Node<>(null, e, f); + first = newNode; + if (f == null) + last = newNode; + else + f.prev = newNode; + size++; + modCount++; +} +``` + +addLast 的内核其实和 addFirst 差不多,就交给大家自行理解了。 + + +**2)招式二:删** + +我这个删的招式还挺多的: + +- `remove()`:删除第一个节点 +- `remove(int)`:删除指定位置的节点 +- `remove(Object)`:删除指定元素的节点 +- `removeFirst()`:删除第一个节点 +- `removeLast()`:删除最后一个节点 + +remove 内部调用的是 removeFirst,所以这两个招式的功效一样。 + +`remove(int)` 内部其实调用的是 unlink 方法。 + +```java +public E remove(int index) { + checkElementIndex(index); + return unlink(node(index)); +} +``` + +unlink 方法其实很好理解,就是更新当前节点的 next 和 prev,然后把当前节点上的元素设为 null。 + +```java +E unlink(Node x) { + // assert x != null; + final E element = x.item; + final Node next = x.next; + final Node prev = x.prev; + + if (prev == null) { + first = next; + } else { + prev.next = next; + x.prev = null; + } + + if (next == null) { + last = prev; + } else { + next.prev = prev; + x.next = null; + } + + x.item = null; + size--; + modCount++; + return element; +} +``` + +remove(Object) 内部也调用了 unlink 方法,只不过在此之前要先找到元素所在的节点: + +```java +public boolean remove(Object o) { + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} +``` + +这内部就分为两种,一种是元素为 null 的时候,必须使用 == 来判断;一种是元素为非 null 的时候,要使用 equals 来判断。equals 是不能用来判 null 的,会抛出 NPE 错误。 + +removeFirst 内部调用的是 unlinkFirst 方法: + +```java +public E removeFirst() { + final Node f = first; + if (f == null) + throw new NoSuchElementException(); + return unlinkFirst(f); +} +``` + +unlinkFirst 负责的就是把第一个节点毁尸灭迹,并且捎带把后一个节点的 prev 设为 null。 + +```java +private E unlinkFirst(Node f) { + // assert f == first && f != null; + final E element = f.item; + final Node next = f.next; + f.item = null; + f.next = null; // help GC + first = next; + if (next == null) + last = null; + else + next.prev = null; + size--; + modCount++; + return element; +} +``` + +**3)招式三:改** + +可以调用 `set()` 方法来更新元素: + +```java +list.set(0, "沉默王五"); +``` + +来看一下 `set()` 方法: + +```java +public E set(int index, E element) { + checkElementIndex(index); + Node x = node(index); + E oldVal = x.item; + x.item = element; + return oldVal; +} +``` + +首先对指定的下标进行检查,看是否越界;然后根据下标查找原有的节点: + +```java +Node node(int index) { + // assert isElementIndex(index); + + if (index < (size >> 1)) { + Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +``` + +`size >> 1`:也就是右移一位,相当于除以 2。对于计算机来说,移位比除法运算效率更高,因为数据在计算机内部都是二进制存储的。 + +换句话说,node 方法会对下标进行一个初步判断,如果靠近前半截,就从下标 0 开始遍历;如果靠近后半截,就从末尾开始遍历。 + +找到指定下标的节点就简单了,直接把原有节点的元素替换成新的节点就 OK 了,prev 和 next 都不用改动。 + +**4)招式四:查** + +我这个查的招式可以分为两种: + +- indexOf(Object):查找某个元素所在的位置 +- get(int):查找某个位置上的元素 + +indexOf 的内部分为两种,一种是元素为 null 的时候,必须使用 == 来判断;一种是元素为非 null 的时候,要使用 equals 来判断。因为 equals 是不能用来判 null 的,会抛出 NPE 错误。 + +```java +public int indexOf(Object o) { + int index = 0; + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) + return index; + index++; + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) + return index; + index++; + } + } + return -1; +} +``` + +get 方法的内核其实还是 node 方法,这个之前已经说明过了,这里略过。 + +```java +public E get(int index) { + checkElementIndex(index); + return node(index).item; +} +``` + +其实,查这个招式还可以演化为其他的一些,比如说: + +- `getFirst()` 方法用于获取第一个元素; +- `getLast()` 方法用于获取最后一个元素; +- `poll()` 和 `pollFirst()` 方法用于删除并返回第一个元素(两个方法尽管名字不同,但方法体是完全相同的); +- `pollLast()` 方法用于删除并返回最后一个元素; +- `peekFirst()` 方法用于返回但不删除第一个元素。 + +### 四、LinkedList 的挑战 + +说句实在话,我不是很喜欢和师兄 ArrayList 拿来比较,因为我们各自修炼的内功不同,没有孰高孰低。 + +虽然师兄经常喊我一声师弟,但我们之间其实挺和谐的。但我知道,在外人眼里,同门师兄弟,总要一较高下的。 + +比如说,我们俩在增删改查时候的时间复杂度。 + +也许这就是命运吧,从我进入师门的那天起,这种争论就一直没有停息过。 + +无论外人怎么看待我们,在我眼里,师兄永远都是一哥,我敬重他,他也愿意保护我。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/list-war-2.md b/docs/collection/list-war-2.md new file mode 100644 index 0000000000..9f97795431 --- /dev/null +++ b/docs/collection/list-war-2.md @@ -0,0 +1,844 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java中ArrayList和LinkedList的区别 + + +### 01、ArrayList 是如何实现的? + +ArrayList 实现了 List 接口,继承了 AbstractList 抽象类。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/list-war-2-01.png) + +底层是基于数组实现的,并且实现了动态扩容 + + +```java +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable +{ + private static final int DEFAULT_CAPACITY = 10; + transient Object[] elementData; + private int size; +} +``` + +ArrayList 还实现了 RandomAccess 接口,这是一个标记接口: + +```java +public interface RandomAccess { +} +``` + +内部是空的,标记“实现了这个接口的类支持快速(通常是固定时间)随机访问”。快速随机访问是什么意思呢?就是说不需要遍历,就可以通过下标(索引)直接访问到内存地址。 + +```java +public E get(int index) { + Objects.checkIndex(index, size); + return elementData(index); +} +E elementData(int index) { + return (E) elementData[index]; +} +``` + +ArrayList 还实现了 Cloneable 接口,这表明 ArrayList 是支持拷贝的。ArrayList 内部的确也重写了 Object 类的 `clone()` 方法。 + +```java +public Object clone() { + try { + ArrayList v = (ArrayList) super.clone(); + v.elementData = Arrays.copyOf(elementData, size); + v.modCount = 0; + return v; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(e); + } +} +``` + +ArrayList 还实现了 Serializable 接口,同样是一个标记接口: + +```java +public interface Serializable { +} +``` + +内部也是空的,标记“实现了这个接口的类支持序列化”。序列化是什么意思呢?Java 的序列化是指,将对象转换成以字节序列的形式来表示,这些字节序中包含了对象的字段和方法。序列化后的对象可以被写到数据库、写到文件,也可用于网络传输。 + +眼睛雪亮的小伙伴可能会注意到,ArrayList 中的关键字段 elementData 使用了 transient 关键字修饰,这个关键字的作用是,让它修饰的字段不被序列化。 + +这不前后矛盾吗?一个类既然实现了 Serilizable 接口,肯定是想要被序列化的,对吧?那为什么保存关键数据的 elementData 又不想被序列化呢? + +这还得从 “ArrayList 是基于数组实现的”开始说起。大家都知道,数组是定长的,就是说,数组一旦声明了,长度(容量)就是固定的,不能像某些东西一样伸缩自如。这就很麻烦,数组一旦装满了,就不能添加新的元素进来了。 + +ArrayList 不想像数组这样活着,它想能屈能伸,所以它实现了动态扩容。一旦在添加元素的时候,发现容量用满了 `s == elementData.length`,就按照原来数组的 1.5 倍(`oldCapacity >> 1`)进行扩容。扩容之后,再将原有的数组复制到新分配的内存地址上 `Arrays.copyOf(elementData, newCapacity)`。 + +```java +private void add(E e, Object[] elementData, int s) { + if (s == elementData.length) + elementData = grow(); + elementData[s] = e; + size = s + 1; +} + +private Object[] grow() { + return grow(size + 1); +} + +private Object[] grow(int minCapacity) { + int oldCapacity = elementData.length; + if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + int newCapacity = ArraysSupport.newLength(oldCapacity, + minCapacity - oldCapacity, /* minimum growth */ + oldCapacity >> 1 /* preferred growth */); + return elementData = Arrays.copyOf(elementData, newCapacity); + } else { + return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; + } +} +``` + +动态扩容意味着什么?大家伙想一下。嗯,还是我来告诉大家答案吧,有点迫不及待。 + +意味着数组的实际大小可能永远无法被填满的,总有多余出来空置的内存空间。 + +比如说,默认的数组大小是 10,当添加第 11 个元素的时候,数组的长度扩容了 1.5 倍,也就是 15,意味着还有 4 个内存空间是闲置的,对吧? + +序列化的时候,如果把整个数组都序列化的话,是不是就多序列化了 4 个内存空间。当存储的元素数量非常非常多的时候,闲置的空间就非常非常大,序列化耗费的时间就会非常非常多。 + +于是,ArrayList 做了一个愉快而又聪明的决定,内部提供了两个私有方法 writeObject 和 readObject 来完成序列化和反序列化。 + +```java +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out element count, and any hidden stuff + int expectedModCount = modCount; + s.defaultWriteObject(); + + // Write out size as capacity for behavioral compatibility with clone() + s.writeInt(size); + + // Write out all elements in the proper order. + for (int i=0; i + extends AbstractSequentialList + implements List, Deque, Cloneable, java.io.Serializable +{ + transient int size = 0; + transient Node first; + transient Node last; +} +``` + + LinkedList 内部定义了一个 Node 节点,它包含 3 个部分:元素内容 item,前引用 prev 和后引用 next。代码如下所示: + +```java +private static class Node { + E item; + LinkedList.Node next; + LinkedList.Node prev; + + Node(LinkedList.Node prev, E element, LinkedList.Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } +} +``` + +LinkedList 还实现了 Cloneable 接口,这表明 LinkedList 是支持拷贝的。 + +LinkedList 还实现了 Serializable 接口,这表明 LinkedList 是支持序列化的。眼睛雪亮的小伙伴可能又注意到了,LinkedList 中的关键字段 size、first、last 都使用了 transient 关键字修饰,这不又矛盾了吗?到底是想序列化还是不想序列化? + +答案是 LinkedList 想按照自己的方式序列化,来看它自己实现的 `writeObject()` 方法: + +```java +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out any hidden serialization magic + s.defaultWriteObject(); + + // Write out size + s.writeInt(size); + + // Write out all elements in the proper order. + for (LinkedList.Node x = first; x != null; x = x.next) + s.writeObject(x.item); +} +``` + +发现没?LinkedList 在序列化的时候只保留了元素的内容 item,并没有保留元素的前后引用。这样就节省了不少内存空间,对吧? + +那有些小伙伴可能就疑惑了,只保留元素内容,不保留前后引用,那反序列化的时候怎么办? + +```java +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + // Read in any hidden serialization magic + s.defaultReadObject(); + + // Read in size + int size = s.readInt(); + + // Read in all elements in the proper order. + for (int i = 0; i < size; i++) + linkLast((E)s.readObject()); +} + +void linkLast(E e) { + final LinkedList.Node l = last; + final LinkedList.Node newNode = new LinkedList.Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} +``` + +注意 for 循环中的 `linkLast()` 方法,它可以把链表重新链接起来,这样就恢复了链表序列化之前的顺序。很妙,对吧? + +和 ArrayList 相比,LinkedList 没有实现 RandomAccess 接口,这是因为 LinkedList 存储数据的内存地址是不连续的,所以不支持随机访问。 + +### 03、ArrayList 和 LinkedList 新增元素时究竟谁快? + +前面我们已经从多个维度了解了 ArrayList 和 LinkedList 的实现原理和各自的特点。那接下来,我们就来聊聊 ArrayList 和 LinkedList 在新增元素时究竟谁快? + +**1)ArrayList** + +ArrayList 新增元素有两种情况,一种是直接将元素添加到数组末尾,一种是将元素插入到指定位置。 + +添加到数组末尾的源码: + +```java +public boolean add(E e) { + modCount++; + add(e, elementData, size); + return true; +} + +private void add(E e, Object[] elementData, int s) { + if (s == elementData.length) + elementData = grow(); + elementData[s] = e; + size = s + 1; +} +``` + +很简单,先判断是否需要扩容,然后直接通过索引将元素添加到末尾。 + +插入到指定位置的源码: + +```java +public void add(int index, E element) { + rangeCheckForAdd(index); + modCount++; + final int s; + Object[] elementData; + if ((s = size) == (elementData = this.elementData).length) + elementData = grow(); + System.arraycopy(elementData, index, + elementData, index + 1, + s - index); + elementData[index] = element; + size = s + 1; +} +``` + +先检查插入的位置是否在合理的范围之内,然后判断是否需要扩容,再把该位置以后的元素复制到新添加元素的位置之后,最后通过索引将元素添加到指定的位置。这种情况是非常伤的,性能会比较差。 + +**2)LinkedList** + +LinkedList 新增元素也有两种情况,一种是直接将元素添加到队尾,一种是将元素插入到指定位置。 + +添加到队尾的源码: + +```java +public boolean add(E e) { + linkLast(e); + return true; +} +void linkLast(E e) { + final LinkedList.Node l = last; + final LinkedList.Node newNode = new LinkedList.Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} +``` + +先将队尾的节点 last 存放到临时变量 l 中(不是说不建议使用 I 作为变量名吗?Java 的作者们明知故犯啊),然后生成新的 Node 节点,并赋给 last,如果 l 为 null,说明是第一次添加,所以 first 为新的节点;否则将新的节点赋给之前 last 的 next。 + +插入到指定位置的源码: + +```java +public void add(int index, E element) { + checkPositionIndex(index); + + if (index == size) + linkLast(element); + else + linkBefore(element, node(index)); +} +LinkedList.Node node(int index) { + // assert isElementIndex(index); + + if (index < (size >> 1)) { + LinkedList.Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + LinkedList.Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +void linkBefore(E e, LinkedList.Node succ) { + // assert succ != null; + final LinkedList.Node pred = succ.prev; + final LinkedList.Node newNode = new LinkedList.Node<>(pred, e, succ); + succ.prev = newNode; + if (pred == null) + first = newNode; + else + pred.next = newNode; + size++; + modCount++; +} +``` + +先检查插入的位置是否在合理的范围之内,然后判断插入的位置是否是队尾,如果是,添加到队尾;否则执行 `linkBefore()` 方法。 + +在执行 `linkBefore()` 方法之前,会调用 `node()` 方法查找指定位置上的元素,这一步是需要遍历 LinkedList 的。如果插入的位置靠前前半段,就从队头开始往后找;否则从队尾往前找。也就是说,如果插入的位置越靠近 LinkedList 的中间位置,遍历所花费的时间就越多。 + +找到指定位置上的元素(succ)之后,就开始执行 `linkBefore()` 方法了,先将 succ 的前一个节点(prev)存放到临时变量 pred 中,然后生成新的 Node 节点(newNode),并将 succ 的前一个节点变更为 newNode,如果 pred 为 null,说明插入的是队头,所以 first 为新节点;否则将 pred 的后一个节点变更为 newNode。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/list-war-2-03.png) + +经过源码分析以后,小伙伴们是不是在想:“好像 ArrayList 在新增元素的时候效率并不一定比 LinkedList 低啊!” + +当两者的起始长度是一样的情况下: + +- 如果是从集合的头部新增元素,ArrayList 花费的时间应该比 LinkedList 多,因为需要对头部以后的元素进行复制。 + +```java +public class ArrayListTest { + public static void addFromHeaderTest(int num) { + ArrayList list = new ArrayList(num); + int i = 0; + + long timeStart = System.currentTimeMillis(); + + while (i < num) { + list.add(0, i + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("ArrayList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class LinkedListTest { + public static void addFromHeaderTest(int num) { + LinkedList list = new LinkedList(); + int i = 0; + long timeStart = System.currentTimeMillis(); + while (i < num) { + list.addFirst(i + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("LinkedList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} +``` + +num 为 10000,代码实测后的时间如下所示: + +``` +ArrayList从集合头部位置新增元素花费的时间595 +LinkedList从集合头部位置新增元素花费的时间15 +``` + +ArrayList 花费的时间比 LinkedList 要多很多。 + +- 如果是从集合的中间位置新增元素,ArrayList 花费的时间搞不好要比 LinkedList 少,因为 LinkedList 需要遍历。 + +```java +public class ArrayListTest { + public static void addFromMidTest(int num) { + ArrayList list = new ArrayList(num); + int i = 0; + + long timeStart = System.currentTimeMillis(); + while (i < num) { + int temp = list.size(); + list.add(temp / 2 + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("ArrayList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} + +public class LinkedListTest { + public static void addFromMidTest(int num) { + LinkedList list = new LinkedList(); + int i = 0; + long timeStart = System.currentTimeMillis(); + while (i < num) { + int temp = list.size(); + list.add(temp / 2, i + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("LinkedList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} +``` + +num 为 10000,代码实测后的时间如下所示: + +``` +ArrayList从集合中间位置新增元素花费的时间1 +LinkedList从集合中间位置新增元素花费的时间101 +``` + +ArrayList 花费的时间比 LinkedList 要少很多很多。 + +- 如果是从集合的尾部新增元素,ArrayList 花费的时间应该比 LinkedList 少,因为数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列。 + +```java +public class ArrayListTest { + public static void addFromTailTest(int num) { + ArrayList list = new ArrayList(num); + int i = 0; + + long timeStart = System.currentTimeMillis(); + + while (i < num) { + list.add(i + "沉默王二"); + i++; + } + + long timeEnd = System.currentTimeMillis(); + + System.out.println("ArrayList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} + +public class LinkedListTest { + public static void addFromTailTest(int num) { + LinkedList list = new LinkedList(); + int i = 0; + long timeStart = System.currentTimeMillis(); + while (i < num) { + list.add(i + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("LinkedList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} +``` + +num 为 10000,代码实测后的时间如下所示: + +``` +ArrayList从集合尾部位置新增元素花费的时间69 +LinkedList从集合尾部位置新增元素花费的时间193 +``` + +ArrayList 花费的时间比 LinkedList 要少一些。 + +这样的结论和预期的是不是不太相符?ArrayList 在添加元素的时候如果不涉及到扩容,性能在两种情况下(中间位置新增元素、尾部新增元素)比 LinkedList 好很多,只有头部新增元素的时候比 LinkedList 差,因为数组复制的原因。 + +当然了,如果涉及到数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。 + +### 04、ArrayList 和 LinkedList 删除元素时究竟谁快? + +**1)ArrayList** + +ArrayList 删除元素的时候,有两种方式,一种是直接删除元素(`remove(Object)`),需要直先遍历数组,找到元素对应的索引;一种是按照索引删除元素(`remove(int)`)。 + +```java +public boolean remove(Object o) { + final Object[] es = elementData; + final int size = this.size; + int i = 0; + found: { + if (o == null) { + for (; i < size; i++) + if (es[i] == null) + break found; + } else { + for (; i < size; i++) + if (o.equals(es[i])) + break found; + } + return false; + } + fastRemove(es, i); + return true; +} +public E remove(int index) { + Objects.checkIndex(index, size); + final Object[] es = elementData; + + @SuppressWarnings("unchecked") E oldValue = (E) es[index]; + fastRemove(es, index); + + return oldValue; +} +``` + +但从本质上讲,都是一样的,因为它们最后调用的都是 `fastRemove(Object, int)` 方法。 + +```java +private void fastRemove(Object[] es, int i) { + modCount++; + final int newSize; + if ((newSize = size - 1) > i) + System.arraycopy(es, i + 1, es, i, newSize - i); + es[size = newSize] = null; +} +``` + +从源码可以看得出,只要删除的不是最后一个元素,都需要数组重组。删除的元素位置越靠前,代价就越大。 + + +**2)LinkedList** + +LinkedList 删除元素的时候,有四种常用的方式: + +- `remove(int)`,删除指定位置上的元素 + +```java +public E remove(int index) { + checkElementIndex(index); + return unlink(node(index)); +} +``` + +先检查索引,再调用 `node(int)` 方法( 前后半段遍历,和新增元素操作一样)找到节点 Node,然后调用 `unlink(Node)` 解除节点的前后引用,同时更新前节点的后引用和后节点的前引用: + +```java + E unlink(Node x) { + // assert x != null; + final E element = x.item; + final Node next = x.next; + final Node prev = x.prev; + + if (prev == null) { + first = next; + } else { + prev.next = next; + x.prev = null; + } + + if (next == null) { + last = prev; + } else { + next.prev = prev; + x.next = null; + } + + x.item = null; + size--; + modCount++; + return element; + } +``` + +- `remove(Object)`,直接删除元素 + +```java +public boolean remove(Object o) { + if (o == null) { + for (LinkedList.Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + for (LinkedList.Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} +``` + +也是先前后半段遍历,找到要删除的元素后调用 `unlink(Node)`。 + +- `removeFirst()`,删除第一个节点 + +```java +public E removeFirst() { + final LinkedList.Node f = first; + if (f == null) + throw new NoSuchElementException(); + return unlinkFirst(f); +} +private E unlinkFirst(LinkedList.Node f) { + // assert f == first && f != null; + final E element = f.item; + final LinkedList.Node next = f.next; + f.item = null; + f.next = null; // help GC + first = next; + if (next == null) + last = null; + else + next.prev = null; + size--; + modCount++; + return element; +} +``` + +删除第一个节点就不需要遍历了,只需要把第二个节点更新为第一个节点即可。 + +- `removeLast()`,删除最后一个节点 + +删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点即可。 + +可以看得出,LinkedList 在删除比较靠前和比较靠后的元素时,非常高效,但如果删除的是中间位置的元素,效率就比较低了。 + +这里就不再做代码测试了,感兴趣的小伙伴可以自己试试,结果和新增元素保持一致: + +- 从集合头部删除元素时,ArrayList 花费的时间比 LinkedList 多很多; + +- 从集合中间位置删除元素时,ArrayList 花费的时间比 LinkedList 少很多; + +- 从集合尾部删除元素时,ArrayList 花费的时间比 LinkedList 少一点。 + +我本地的统计结果如下所示,小伙伴们可以作为参考: + +``` +ArrayList从集合头部位置删除元素花费的时间380 +LinkedList从集合头部位置删除元素花费的时间4 +ArrayList从集合中间位置删除元素花费的时间381 +LinkedList从集合中间位置删除元素花费的时间5922 +ArrayList从集合尾部位置删除元素花费的时间8 +LinkedList从集合尾部位置删除元素花费的时间12 +``` + +### 05、ArrayList 和 LinkedList 遍历元素时究竟谁快? + +**1)ArrayList** + +遍历 ArrayList 找到某个元素的话,通常有两种形式: + +- `get(int)`,根据索引找元素 + +```java +public E get(int index) { + Objects.checkIndex(index, size); + return elementData(index); +} +``` + +由于 ArrayList 是由数组实现的,所以根据索引找元素非常的快,一步到位。 + +- `indexOf(Object)`,根据元素找索引 + +```java +public int indexOf(Object o) { + return indexOfRange(o, 0, size); +} + +int indexOfRange(Object o, int start, int end) { + Object[] es = elementData; + if (o == null) { + for (int i = start; i < end; i++) { + if (es[i] == null) { + return i; + } + } + } else { + for (int i = start; i < end; i++) { + if (o.equals(es[i])) { + return i; + } + } + } + return -1; +} +``` + +根据元素找索引的话,就需要遍历整个数组了,从头到尾依次找。 + + +**2)LinkedList** + +遍历 LinkedList 找到某个元素的话,通常也有两种形式: + +- `get(int)`,找指定位置上的元素 + +```java +public E get(int index) { + checkElementIndex(index); + return node(index).item; +} +``` + +既然需要调用 `node(int)` 方法,就意味着需要前后半段遍历了。 + +- `indexOf(Object)`,找元素所在的位置 + +```java +public int indexOf(Object o) { + int index = 0; + if (o == null) { + for (LinkedList.Node x = first; x != null; x = x.next) { + if (x.item == null) + return index; + index++; + } + } else { + for (LinkedList.Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) + return index; + index++; + } + } + return -1; +} +``` + +需要遍历整个链表,和 ArrayList 的 `indexOf()` 类似。 + +那在我们对集合遍历的时候,通常有两种做法,一种是使用 for 循环,一种是使用迭代器(Iterator)。 + +如果使用的是 for 循环,可想而知 LinkedList 在 get 的时候性能会非常差,因为每一次外层的 for 循环,都要执行一次 `node(int)` 方法进行前后半段的遍历。 + +```java +LinkedList.Node node(int index) { + // assert isElementIndex(index); + + if (index < (size >> 1)) { + LinkedList.Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + LinkedList.Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +``` + + + +那如果使用的是迭代器呢? + +```java +LinkedList list = new LinkedList(); +for (Iterator it = list.iterator(); it.hasNext();) { + it.next(); +} +``` + +迭代器只会调用一次 `node(int)` 方法,在执行 `list.iterator()` 的时候:先调用 AbstractSequentialList 类的 `iterator()` 方法,再调用 AbstractList 类的 `listIterator()` 方法,再调用 LinkedList 类的 `listIterator(int)` 方法,如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/list-war-2-04.png) + +最后返回的是 LinkedList 类的内部私有类 ListItr 对象: + +```java +public ListIterator listIterator(int index) { + checkPositionIndex(index); + return new LinkedList.ListItr(index); +} + +private class ListItr implements ListIterator { + private LinkedList.Node lastReturned; + private LinkedList.Node next; + private int nextIndex; + private int expectedModCount = modCount; + + ListItr(int index) { + // assert isPositionIndex(index); + next = (index == size) ? null : node(index); + nextIndex = index; + } + + public boolean hasNext() { + return nextIndex < size; + } + + public E next() { + checkForComodification(); + if (!hasNext()) + throw new NoSuchElementException(); + + lastReturned = next; + next = next.next; + nextIndex++; + return lastReturned.item; + } +} +``` + +执行 ListItr 的构造方法时调用了一次 `node(int)` 方法,返回第一个节点。在此之后,迭代器就执行 `hasNext()` 判断有没有下一个,执行 `next()` 方法下一个节点。 + +由此,可以得出这样的结论:**遍历 LinkedList 的时候,千万不要使用 for 循环,要使用迭代器。** + +也就是说,for 循环遍历的时候,ArrayList 花费的时间远小于 LinkedList;迭代器遍历的时候,两者性能差不多。 + +### 06、总结 + +花了两天时间,终于肝完了!相信看完这篇文章后,再有面试官问你 ArrayList 和 LinkedList 有什么区别的话,你一定会胸有成竹地和他扯上半小时了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/common-tool/arrays.md b/docs/common-tool/arrays.md similarity index 82% rename from docs/src/common-tool/arrays.md rename to docs/common-tool/arrays.md index 09bdb0c19e..55893c4073 100644 --- a/docs/src/common-tool/arrays.md +++ b/docs/common-tool/arrays.md @@ -1,19 +1,14 @@ --- -title: Java Arrays:专为数组而生的工具类 -shortTitle: Arrays工具类 category: - Java核心 tag: - - 常用工具类 -description: 本文详细介绍了Java中的Arrays工具类,阐述了它在数组操作中的实际应用和优势。通过具体的代码示例,展示了如何使用Arrays类处理数组排序、查找、转换等常见问题。学习Arrays工具类的技巧,让您在Java编程中轻松应对各种数组操作,提高开发效率。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Arrays,数组,java arrays,java 数组 + - Java --- +# Java Arrays工具类10大常用方法 -“哥,数组专用工具类是专门用来操作[数组](https://javabetter.cn/array/array.html)的吗?比如说创建数组、数组排序、数组检索等等。”三妹的提问其实已经把答案说了出来。 + +“哥,数组专用工具类是专门用来操作数组的吗?比如说创建数组、数组排序、数组检索等等。”三妹的提问其实已经把答案说了出来。 “是滴,这里说的数组专用工具类指的是 `java.util.Arrays` 类,基本上常见的数组操作,这个类都提供了静态方法可供直接调用。毕竟数组本身想完成这些操作还是挺麻烦的,有了这层封装,就方便多了。”在回答三妹的同时,我打开 Intellij IDEA,找到了 Arrays 类的源码。 @@ -50,9 +45,7 @@ public class Arrays {} - copyOfRange,复制指定范围内的数组到一个新的数组 - fill,对数组进行填充 -#### 1)copyOf - -直接来看例子: +1)copyOf,直接来看例子: ```java String[] intro = new String[] { "沉", "默", "王", "二" }; @@ -80,9 +73,7 @@ private Object[] grow(int minCapacity) { } ``` -#### 2)copyOfRange - -直接来看例子: +2)copyOfRange,直接来看例子: ```java String[] intro = new String[] { "沉", "默", "王", "二" }; @@ -115,9 +106,8 @@ System.out.println(Arrays.toString(abridgementExpanded)); “嗯,我想是 Arrays 的设计者考虑到了数组越界的问题,不然每次调用 Arrays 类就要先判断很多次长度,很麻烦。”稍作思考后,我给出了这样一个回答。 -#### 3)fill -直接来看例子: +3)fill,直接来看例子: ```java String[] stutter = new String[4]; @@ -176,7 +166,7 @@ public static boolean equals(Object[] a, Object[] a2) { } ``` -因为数组是一个对象,所以先使用“==”操作符进行判断,如果不相等,再判断是否为 null,其中一个为 null,返回 false;紧接着判断 length,不等的话,返回 false;否则的话,依次调用 `Objects.equals()` 比较相同位置上的元素是否相等。 +因为数组是一个对象,所以先使用“==”操作符进行判断,如果不相等,再判断是否为 null,两个都为 null,返回 false;紧接着判断 length,不等的话,返回 false;否则的话,依次调用 `Objects.equals()` 比较相同位置上的元素是否相等。 “这段代码还是非常严谨的,对吧?三妹,这也就是我们学习源码的意义,欣赏的同时,可以学习源码作者清晰的编码思路。”我语重心长地给三妹讲。 @@ -268,7 +258,7 @@ System.out.println(caseInsensitive); “流是什么呀?”三妹好奇的问。 -“流的英文单词是 Stream,它可以极大提高 Java 程序员的生产力,让程序员写出高效、干净、简洁的代码。 这种风格将要处理的集合看作是一种流,想象一下水流在管道中流过的样子,我们可以在管道中对流进行处理,比如筛选、排序等等。[Stream 具体怎么使用](https://javabetter.cn/java8/stream.html),我们留到后面再详细地讲,这里你先有一个大致的印象就可以了。”我回答到。 +“流的英文单词是 Stream,它可以极大提高 Java 程序员的生产力,让程序员写出高效、干净、简洁的代码。 这种风格将要处理的集合看作是一种流,想象一下水流在管道中流过的样子,我们可以在管道中对流进行处理,比如筛选、排序等等。Stream 具体怎么使用,我们留到后面再详细地讲,这里你先有一个大致的印象就可以了。”我回答到。 Arrays 类的 `stream()` 方法可以将数组转换成流: @@ -299,7 +289,7 @@ Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: origin(2) > [Ljava.lang.String;@3d075dc0 ``` -[最优雅的打印方式](https://javabetter.cn/array/print.html),是使用 `Arrays.toString()`,其实前面讲过。来看一下该方法的源码: +最优雅的打印方式,是使用 `Arrays.toString()`,来看一下该方法的源码: ```java public static String toString(Object[] a) { @@ -337,7 +327,7 @@ public static String toString(Object[] a) { ### 07、数组转 List -尽管数组非常强大,但它自身可以操作的工具方法很少,比如说判断数组中是否包含某个值。如果能转成 List 的话,就简便多了,因为 Java 的[集合框架 List](https://javabetter.cn/collection/gailan.html) 中封装了很多常用的方法。 +尽管数组非常强大,但它自身可以操作的工具方法很少,比如说判断数组中是否包含某个值。如果能转成 List 的话,就简便多了,因为 Java 的集合框架 List 中封装了很多常用的方法。 ```java String[] intro = new String[] { "沉", "默", "王", "二" }; @@ -345,7 +335,7 @@ List rets = Arrays.asList(intro); System.out.println(rets.contains("二")); ``` -不过需要注意的是,`Arrays.asList()` 返回的是 `java.util.Arrays.ArrayList`,并不是 [`java.util.ArrayList`](https://javabetter.cn/collection/arraylist.html),它的长度是固定的,无法进行元素的删除或者添加。 +不过需要注意的是,`Arrays.asList()` 返回的是 `java.util.Arrays.ArrayList`,并不是 `java.util.ArrayList`,它的长度是固定的,无法进行元素的删除或者添加。 ```java rets.add("三"); @@ -370,7 +360,7 @@ rets1.remove("二"); ### 08、setAll -Java 8 新增了 `setAll()` 方法,它提供了一个[函数式编程](https://javabetter.cn/java8/Lambda.html)的入口,可以对数组的元素进行填充: +Java 8 新增了 `setAll()` 方法,它提供了一个函数式编程的入口,可以对数组的元素进行填充: ```java int[] array = new int[10]; @@ -433,11 +423,4 @@ System.out.println(Arrays.toString(arr)); 我来到客厅,坐到沙发上,捧起黄永玉先生的《无愁河上的浪荡汉子·八年卷 1》看了起来,津津有味。。。。。。 ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/common-tool/collections.md b/docs/common-tool/collections.md new file mode 100644 index 0000000000..b8121a3eb1 --- /dev/null +++ b/docs/common-tool/collections.md @@ -0,0 +1,256 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java集合框架:Collections工具类 + + +Collections 是 JDK 提供的一个工具类,位于 java.util 包下,提供了一系列的静态方法,方便我们对集合进行各种骚操作,算是集合框架的一个大管家。 + +还记得我们前面讲过的 [Arrays 工具类](https://mp.weixin.qq.com/s/9dYmKXEErZbyPJ_GxwWYug)吗?可以回去温习下。 + +Collections 的用法很简单,在 Intellij IDEA 中敲完 `Collections.` 之后就可以看到它提供的方法了,大致看一下方法名和参数就能知道这个方法是干嘛的。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/collections-01.png) + +为了节省大家的学习时间,我将这些方法做了一些分类,并列举了一些简单的例子。 + +### 01、排序操作 + +- `reverse(List list)`:反转顺序 +- `shuffle(List list)`:洗牌,将顺序打乱 +- `sort(List list)`:自然升序 +- `sort(List list, Comparator c)`:按照自定义的比较器排序 +- `swap(List list, int i, int j)`:将 i 和 j 位置的元素交换位置 + +来看例子: + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("沉默王四"); +list.add("沉默王五"); +list.add("沉默王六"); + +System.out.println("原始顺序:" + list); + +// 反转 +Collections.reverse(list); +System.out.println("反转后:" + list); + +// 洗牌 +Collections.shuffle(list); +System.out.println("洗牌后:" + list); + +// 自然升序 +Collections.sort(list); +System.out.println("自然升序后:" + list); + +// 交换 +Collections.swap(list, 2,4); +System.out.println("交换后:" + list); +``` + +输出后: + +``` +原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六] +反转后:[沉默王六, 沉默王五, 沉默王四, 沉默王三, 沉默王二] +洗牌后:[沉默王五, 沉默王二, 沉默王六, 沉默王三, 沉默王四] +自然升序后:[沉默王三, 沉默王二, 沉默王五, 沉默王六, 沉默王四] +交换后:[沉默王三, 沉默王二, 沉默王四, 沉默王六, 沉默王五] +``` + +### 02、查找操作 + +- `binarySearch(List list, Object key)`:二分查找法,前提是 List 已经排序过了 +- `max(Collection coll)`:返回最大元素 +- `max(Collection coll, Comparator comp)`:根据自定义比较器,返回最大元素 +- `min(Collection coll)`:返回最小元素 +- `min(Collection coll, Comparator comp)`:根据自定义比较器,返回最小元素 +- `fill(List list, Object obj)`:使用指定对象填充 +- `frequency(Collection c, Object o)`:返回指定对象出现的次数 + +来看例子: + +```java +System.out.println("最大元素:" + Collections.max(list)); +System.out.println("最小元素:" + Collections.min(list)); +System.out.println("出现的次数:" + Collections.frequency(list, "沉默王二")); + +// 没有排序直接调用二分查找,结果是不确定的 +System.out.println("排序前的二分查找结果:" + Collections.binarySearch(list, "沉默王二")); +Collections.sort(list); +// 排序后,查找结果和预期一致 +System.out.println("排序后的二分查找结果:" + Collections.binarySearch(list, "沉默王二")); + +Collections.fill(list, "沉默王八"); +System.out.println("填充后的结果:" + list); +``` + +输出后: + +``` +原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六] +最大元素:沉默王四 +最小元素:沉默王三 +出现的次数:1 +排序前的二分查找结果:0 +排序后的二分查找结果:1 +填充后的结果:[沉默王八, 沉默王八, 沉默王八, 沉默王八, 沉默王八] +``` + +### 03、同步控制 + +[HashMap 是线程不安全](https://mp.weixin.qq.com/s/qk_neCdzM3aB6pVWVTHhNw)的,这个我们前面讲到了。那其实 ArrayList 也是线程不安全的,没法在多线程环境下使用,那 Collections 工具类中提供了多个 synchronizedXxx 方法,这些方法会返回一个同步的对象,从而解决多线程中访问集合时的安全问题。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/collections-02.png) + +使用起来也非常的简单: + +```java +SynchronizedList synchronizedList = Collections.synchronizedList(list); +``` + +看一眼 SynchronizedList 的源码就明白了,不过是在方法里面使用 synchronized 关键字加了一层锁而已。 + +```java +static class SynchronizedList + extends SynchronizedCollection + implements List { + private static final long serialVersionUID = -7754090372962971524L; + + final List list; + + SynchronizedList(List list) { + super(list); + this.list = list; + } + + public E get(int index) { + synchronized (mutex) {return list.get(index);} + } + + public void add(int index, E element) { + synchronized (mutex) {list.add(index, element);} + } + public E remove(int index) { + synchronized (mutex) {return list.remove(index);} + } +} +``` + +那这样的话,其实效率和那些直接在方法上加 synchronized 关键字的 Vector、Hashtable 差不多(JDK 1.0 时期就有了),而这些集合类基本上已经废弃了,几乎不怎么用。 + +```java +public class Vector + extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable +{ + + public synchronized E get(int index) { + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + + return elementData(index); + } + + public synchronized E remove(int index) { + modCount++; + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + E oldValue = elementData(index); + + int numMoved = elementCount - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--elementCount] = null; // Let gc do its work + + return oldValue; + } +} +``` + +正确的做法是使用并发包下的 CopyOnWriteArrayList、ConcurrentHashMap。这些我们放到并发编程时再讲。 + +### 04、不可变集合 + +- `emptyXxx()`:制造一个空的不可变集合 +- `singletonXxx()`:制造一个只有一个元素的不可变集合 +- `unmodifiableXxx()`:为指定集合制作一个不可变集合 + +举个例子: + +```java +List emptyList = Collections.emptyList(); +emptyList.add("非空"); +System.out.println(emptyList); +``` + +这段代码在执行的时候就抛出错误了。 + +``` +Exception in thread "main" java.lang.UnsupportedOperationException + at java.util.AbstractList.add(AbstractList.java:148) + at java.util.AbstractList.add(AbstractList.java:108) + at com.itwanger.s64.Demo.main(Demo.java:61) +``` + +这是因为 `Collections.emptyList()` 会返回一个 Collections 的内部类 EmptyList,而 EmptyList 并没有重写父类 AbstractList 的 `add(int index, E element)` 方法,所以执行的时候就抛出了不支持该操作的 UnsupportedOperationException 了。 + +这是从分析 add 方法源码得出的原因。除此之外,emptyList 方法是 final 的,返回的 EMPTY_LIST 也是 final 的,种种迹象表明 emptyList 返回的就是不可变对象,没法进行增伤改查。 + +```java +public static final List emptyList() { + return (List) EMPTY_LIST; +} + +public static final List EMPTY_LIST = new EmptyList<>(); +``` + +### 05、其他 + +还有两个方法比较常用: + +- `addAll(Collection c, T... elements)`,往集合中添加元素 +- `disjoint(Collection c1, Collection c2)`,判断两个集合是否没有交集 + +举个例子: + +```java +List allList = new ArrayList<>(); +Collections.addAll(allList, "沉默王九","沉默王十","沉默王二"); +System.out.println("addAll 后:" + allList); + +System.out.println("是否没有交集:" + (Collections.disjoint(list, allList) ? "是" : "否")); +``` + +输出后: + +``` +原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六] +addAll 后:[沉默王九, 沉默王十, 沉默王二] +是否没有交集:否 +``` + +整体上,Collections 工具类作为集合框架的大管家,提供了一些非常便利的方法供我们调用,也非常容易掌握,没什么难点,看看方法的注释就能大致明白干嘛的。 + +不过,工具就放在那里,用是一回事,为什么要这么用就是另外一回事了。能不能提高自己的编码水平,很大程度上取决于你到底有没有去钻一钻源码,看这些设计 JDK 的大师们是如何写代码的,学会一招半式,在工作当中还是能很快脱颖而出的。 + +恐怕 JDK 的设计者是这个世界上最好的老师了,文档写得不能再详细了,代码写得不能再优雅了,基本上都达到了性能上的极致。 + +可能有人会说,工具类没什么鸟用,不过是调用下方法而已,但这就大错特错了:如果要你来写,你能写出来 Collections 这样一个工具类吗? + +这才是高手要思考的一个问题。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + + diff --git a/docs/common-tool/guava.md b/docs/common-tool/guava.md new file mode 100644 index 0000000000..b8fd331748 --- /dev/null +++ b/docs/common-tool/guava.md @@ -0,0 +1,256 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Google开源的Guava工具库,太强大了 + + +### 01、前世今生 + +你好呀,我是 Guava。 + +我由 Google 公司开源,目前在 GitHub 上已经有 39.9k 的铁粉了,由此可以证明我的受欢迎程度。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/guava-01.png) + + +我的身体里主要包含有这些常用的模块:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等。新版的 JDK 中已经直接把我引入了,可想而知我有多优秀,忍不住骄傲了。 + +这么说吧,学好如何使用我,能让你在编程中变得更快乐,写出更优雅的代码! + +*PS:star 这种事,只能求,不求没效果😭😭😭。二哥开源的《Java 程序员进阶之路》专栏在 GitHub 上已经收获了 595 枚星标,铁粉们赶紧去点点啦,帮二哥冲 600 star,笔芯*! + +>https://github.com/itwanger/toBeBetterJavaer + +### 02、引入 Guava + +如果你要在 Maven 项目使用我的话,需要先在 pom.xml 文件中引入我的依赖。 + +``` + + com.google.guava + guava + 30.1-jre + +``` + +一点要求,JDK 版本需要在 8 以上。 + +### 03、基本工具 + +Doug Lea,java.util.concurrent 包的作者,曾说过一句话:“null 真糟糕”。Tony Hoare,图灵奖得主、快速排序算法的作者,当然也是 null 的创建者,也曾说过类似的话:“null 的使用,让我损失了十亿美元。”鉴于此,我用 Optional 来表示可能为 null 的对象。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/guava-02.png) + + +代码示例如下所示。 + +```java +Optional possible = Optional.of(5); +possible.isPresent(); // returns true +possible.get(); // returns 5 +``` + +我大哥 Java 在 JDK 8 中新增了 [Optional 类](https://mp.weixin.qq.com/s/PqK0KNVHyoEtZDtp5odocA),显然是从我这借鉴过去的,不过他的和我的有些不同。 + +- 我的 Optional 是 abstract 的,意味着我可以有子类对象;我大哥的是 final 的,意味着没有子类对象。 + +- 我的 Optional 实现了 Serializable 接口,可以序列化;我大哥的没有。 + +- 我的一些方法和我大哥的也不尽相同。 + +使用 Optional 除了赋予 null 语义,增加了可读性,最大的优点在于它是一种傻瓜式的防护。Optional 迫使你积极思考引用缺失的情况,因为你必须显式地从 Optional 获取引用。 + +除了 Optional 之外,我还提供了: + +- 参数校验 +- 常见的 Object 方法,比如说 Objects.equals、Objects.hashCode,JDK 7 引入的 Objects 类提供同样的方法,当然也是从我这借鉴的灵感。 +- 更强大的比较器 + +### 04、集合 + +首先我来说一下,为什么需要不可变集合。 + +- 保证线程安全。在并发程序中,使用不可变集合既保证线程的安全性,也大大地增强了并发时的效率(跟并发锁方式相比)。 + +- 如果一个对象不需要支持修改操作,不可变的集合将会节省空间和时间的开销。 + +- 可以当作一个常量来对待,并且集合中的对象在以后也不会被改变。 + +与 JDK 中提供的不可变集合相比,我提供的 Immutable 才是真正的不可变,我为什么这么说呢?来看下面这个示例。 + +下面的代码利用 JDK 的 `Collections.unmodifiableList(list)` 得到一个不可修改的集合 unmodifiableList。 + +```java +List list = new ArrayList(); +list.add("雷军"); +list.add("乔布斯"); + +List unmodifiableList = Collections.unmodifiableList(list); +unmodifiableList.add("马云"); +``` + +运行代码将会出现以下异常: + +``` +Exception in thread "main" java.lang.UnsupportedOperationException + at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1060) + at com.itwanger.guava.NullTest.main(NullTest.java:29) +``` + +很好,执行 `unmodifiableList.add()` 的时候抛出了 UnsupportedOperationException 异常,说明 `Collections.unmodifiableList()` 返回了一个不可变集合。但真的是这样吗? + +你可以把 `unmodifiableList.add()` 换成 `list.add()`。 + +```java +List list = new ArrayList(); +list.add("雷军"); +list.add("乔布斯"); + +List unmodifiableList = Collections.unmodifiableList(list); +list.add("马云"); +``` + +再次执行的话,程序并没有报错,并且你会发现 unmodifiableList 中真的多了一个元素。说明什么呢? + +`Collections.unmodifiableList(…)` 实现的不是真正的不可变集合,当原始集合被修改后,不可变集合里面的元素也是跟着发生变化。 + +我就不会犯这种错,来看下面的代码。 + +```java +List stringArrayList = Lists.newArrayList("雷军","乔布斯"); +ImmutableList immutableList = ImmutableList.copyOf(stringArrayList); +immutableList.add("马云"); +``` + +尝试 `immutableList.add()` 的时候会抛出 `UnsupportedOperationException`。我在源码中已经把 `add()` 方法废弃了。 + +```java + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + * @deprecated Unsupported operation. + */ + @CanIgnoreReturnValue + @Deprecated + @Override + public final boolean add(E e) { + throw new UnsupportedOperationException(); + } +``` + +尝试 `stringArrayList.add()` 修改原集合的时候 immutableList 并不会因此而发生改变。 + +除了不可变集合以外,我还提供了新的集合类型,比如说: + +- Multiset,可以多次添加相等的元素。当把 Multiset 看成普通的 Collection 时,它表现得就像无序的 ArrayList;当把 Multiset 看作 `Map` 时,它也提供了符合性能期望的查询操作。 + +- Multimap,可以很容易地把一个键映射到多个值。 + +- BiMap,一种特殊的 Map,可以用 `inverse()` 反转 + `BiMap` 的键值映射;保证值是唯一的,因此 `values()` 返回 Set 而不是普通的 Collection。 + + + +### 05、字符串处理 + +字符串表示字符的不可变序列,创建后就不能更改。在我们日常的工作中,字符串的使用非常频繁,熟练的对其操作可以极大的提升我们的工作效率。 + +我提供了连接器——Joiner,可以用分隔符把字符串序列连接起来。下面的代码将会返回“雷军; 乔布斯”,你可以使用 `useForNull(String)` 方法用某个字符串来替换 null,而不像 `skipNulls()` 方法那样直接忽略 null。 + +```java +Joiner joiner = Joiner.on("; ").skipNulls(); +return joiner.join("雷军", null, "乔布斯"); +``` + +我还提供了拆分器—— Splitter,可以按照指定的分隔符把字符串序列进行拆分。 + +```java +Splitter.on(',') + .trimResults() + .omitEmptyStrings() + .split("雷军,乔布斯,, 沉默王二"); +``` + +### 06、缓存 + +缓存在很多场景下都是相当有用的。你应该知道,检索一个值的代价很高,尤其是需要不止一次获取值的时候,就应当考虑使用缓存。 + +我提供的 Cache 和 ConcurrentMap 很相似,但也不完全一样。最基本的区别是 ConcurrentMap 会一直保存所有添加的元素,直到显式地移除。相对地,我提供的 Cache 为了限制内存占用,通常都设定为自动回收元素。 + +如果你愿意消耗一些内存空间来提升速度,你能预料到某些键会被查询一次以上,缓存中存放的数据总量不会超出内存容量,就可以使用 Cache。 + +来个示例你感受下吧。 + +```java +@Test +public void testCache() throws ExecutionException, InterruptedException { + + CacheLoader cacheLoader = new CacheLoader() { + // 如果找不到元素,会调用这里 + @Override + public Animal load(String s) { + return null; + } + }; + LoadingCache loadingCache = CacheBuilder.newBuilder() + .maximumSize(1000) // 容量 + .expireAfterWrite(3, TimeUnit.SECONDS) // 过期时间 + .removalListener(new MyRemovalListener()) // 失效监听器 + .build(cacheLoader); // + loadingCache.put("狗", new Animal("旺财", 1)); + loadingCache.put("猫", new Animal("汤姆", 3)); + loadingCache.put("狼", new Animal("灰太狼", 4)); + + loadingCache.invalidate("猫"); // 手动失效 + + Animal animal = loadingCache.get("狼"); + System.out.println(animal); + Thread.sleep(4 * 1000); + // 狼已经自动过去,获取为 null 值报错 + System.out.println(loadingCache.get("狼")); +} + +/** + * 缓存移除监听器 + */ +class MyRemovalListener implements RemovalListener { + + @Override + public void onRemoval(RemovalNotification notification) { + String reason = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause()); + System.out.println(reason); + } +} + +class Animal { + private String name; + private Integer age; + + public Animal(String name, Integer age) { + this.name = name; + this.age = age; + } +} +``` + +CacheLoader 中重写了 load 方法,这个方法会在查询缓存没有命中时被调用,我这里直接返回了 null,其实这样会在没有命中时抛出 CacheLoader returned null for key 异常信息。 + +MyRemovalListener 作为缓存元素失效时的监听类,在有元素缓存失效时会自动调用 onRemoval 方法,这里需要注意的是这个方法是同步方法,如果这里耗时较长,会阻塞直到处理完成。 + +LoadingCache 就是缓存的主要操作对象了,常用的就是其中的 put 和 get 方法了。 + +### 07、尾声 + +上面介绍了我认为最常用的功能,作为 Google 公司开源的 Java 开发核心库,个人觉得实用性还是很高的(不然呢?嘿嘿嘿)。引入到你的项目后不仅能快速的实现一些开发中常用的功能,而且还可以让代码更加的优雅简洁。 + +我觉得适用于每一个 Java 项目,至于其他的一些功能,比如说散列、事件总线、数学运算、反射,就等待你去发掘了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/guava-03.png) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/common-tool/hutool.md b/docs/common-tool/hutool.md similarity index 84% rename from docs/src/common-tool/hutool.md rename to docs/common-tool/hutool.md index d231603426..13e954d405 100644 --- a/docs/src/common-tool/hutool.md +++ b/docs/common-tool/hutool.md @@ -1,25 +1,21 @@ --- -title: Hutool:国产良心工具包,让你的Java变得更甜 -shortTitle: Hutool工具类库 category: - Java核心 tag: - - 常用工具类 -description: 本文详细介绍了国产Java工具包Hutool,阐述了它在简化Java编程中的实际应用和优势。通过具体的代码示例,展示了如何使用Hutool解决字符串处理、集合操作、日期时间处理等常见问题。学习Hutool的技巧,让您在Java编程中更加轻松、高效,享受编程的乐趣。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Hutool,java hutool + - Java --- +# Hutool:国产良心工具包,让你的Java变得更甜 + 读者群里有个小伙伴感慨说,“Hutool 这款开源类库太厉害了,基本上该有该的工具类,它里面都有。”讲真的,我平常工作中也经常用 Hutool,它确实可以帮助我们简化每一行代码,使 Java 拥有函数式语言般的优雅,让 Java 语言变得“甜甜的”。 -Hutool 的作者在[官网](https://hutool.cn/)上说,Hutool 是 Hu+tool 的自造词(好像不用说,我们也能猜得到),“Hu”用来致敬他的“前任”公司,“tool”就是工具的意思,谐音就有意思了,“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”(一个开源类库,上升到了哲学的高度,作者厉害了)。 +Hutool 的作者在官网上说,Hutool 是 Hu+tool 的自造词(好像不用说,我们也能猜得到),“Hu”用来致敬他的“前任”公司,“tool”就是工具的意思,谐音就有意思了,“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”(一个开源类库,上升到了哲学的高度,作者厉害了)。 看了一下开发团队的一个成员介绍,一个 Java 后端工具的作者竟然爱前端、爱数码,爱美女,嗯嗯嗯,确实“难得糊涂”(手动狗头)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-01.png) + 废话就说到这,来吧,实操走起! @@ -40,11 +36,11 @@ Hutool 的设计思想是尽量减少重复的定义,让项目中的 util 包 就像作者在官网上说的那样: - 以前,我们打开搜索引擎 -> 搜“Java MD5 加密” -> 打开某篇博客 -> 复制粘贴 -> 改改,变得好用些 -- 有了 Hutool 以后呢,引入 Hutool -> 直接 `SecureUtil.md5()` +>有了 Hutool 以后呢,引入 Hutool -> 直接 `SecureUtil.md5()` Hutool 对不仅对 JDK 底层的文件、流、加密解密、转码、正则、线程、XML等做了封装,还提供了以下这些组件: -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-02.png) 非常多,非常全面,鉴于此,我只挑选一些我喜欢的来介绍下(偷偷地告诉你,我就是想偷懒)。 @@ -137,7 +133,7 @@ String chineseZodiac = DateUtil.getChineseZodiac(1989); ### 04、IO 流相关 -[IO 操作包括读和写](https://javabetter.cn/io/shangtou.html),应用的场景主要包括网络操作和文件操作,原生的 Java 类库区分[字符流](https://javabetter.cn/io/reader-writer.html)和[字节流](https://javabetter.cn/io/stream.html),字节流 InputStream 和 OutputStream 就有很多很多种,使用起来让人头皮发麻。 +IO 操作包括读和写,应用的场景主要包括网络操作和文件操作,原生的 Java 类库区分字符流和字节流,字节流 InputStream 和 OutputStream 就有很多很多种,使用起来让人头皮发麻。 Hutool 封装了流操作工具类 IoUtil、文件读写操作工具类 FileUtil、文件类型判断工具类 FileTypeUtil 等等。 @@ -160,11 +156,11 @@ long copySize = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE); 在实际编码当中,我们通常需要从某些文件里面读取一些数据,比如配置文件、文本文件、图片等等,那这些文件通常放在什么位置呢? -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-03.png) 放在项目结构图中的 resources 目录下,当项目编译后,会出现在 classes 目录下。对应磁盘上的目录如下图所示: -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-04.png) 当我们要读取文件的时候,我是不建议使用绝对路径的,因为操作系统不一样的话,文件的路径标识符也是不一样的。最好使用相对路径。 @@ -317,7 +313,7 @@ public class ConsoleDemo { - 是不是电话号码 - 等等 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-05.png) ```java Validator.isEmail("沉默王二"); @@ -326,7 +322,7 @@ Validator.isMobile("itwanger.com"); ### 12、双向查找 Map -[Guava](https://javabetter.cn/common-tool/guava.html) 中提供了一种特殊的 Map 结构,叫做 BiMap,实现了一种双向查找的功能,可以根据 key 查找 value,也可以根据 value 查找 key,Hutool 也提供这种 Map 结构。 +Guava 中提供了一种特殊的 Map 结构,叫做 BiMap,实现了一种双向查找的功能,可以根据 key 查找 value,也可以根据 value 查找 key,Hutool 也提供这种 Map 结构。 ```java BiMap biMap = new BiMap<>(new HashMap<>()); @@ -344,7 +340,7 @@ biMap.getKey("沉默王三"); 在实际的开发工作中,其实我更倾向于使用 Guava 的 BiMap,而不是 Hutool 的。这里提一下,主要是我发现了 Hutool 在线文档上的一处错误,提了个 issue(从中可以看出我一颗一丝不苟的心和一双清澈明亮的大眼睛啊)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-06.png) ### 13、图片工具 @@ -386,7 +382,7 @@ ImgUtil.pressText(// 趁机让大家欣赏一下二哥帅气的真容。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/common-tool/hutool-07.png) ### 14、配置文件 @@ -554,13 +550,5 @@ Hutool 中的类库还有很多,尤其是一些对第三方类库的进一步 项目源码地址:[https://github.com/looly/hutool](https://github.com/looly/hutool) ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - + diff --git a/docs/cs/os.md b/docs/cs/os.md new file mode 100644 index 0000000000..dc0177e843 --- /dev/null +++ b/docs/cs/os.md @@ -0,0 +1,1340 @@ +--- +category: + - 计算机基础 +tag: + - 操作系统 +--- + +# 计算机操作系统知识点大梳理 + +>作者:月伴飞鱼,转载链接:[https://mp.weixin.qq.com/s/G9ZqwEMxjrG5LbgYwM5ACQ](https://mp.weixin.qq.com/s/G9ZqwEMxjrG5LbgYwM5ACQ) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-5ad16ae7-059f-44f1-8236-697b203bb72e.png) + +## 计算机结构 + +现代计算机模型是基于-**冯诺依曼计算机模型** + +计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存储器中取出数据进行指定的运算和逻辑操作等加工,然后再按地址把结果送到内存中去,接下来,再取出第二条指令,在控制器的指挥下完成规定操作,依此进行下去。直至遇到停止指令 + +程序与数据一样存贮,按程序编排的顺序,一步一步地取出指令,自动地完成指令规定的操作是计算机最基本的工作模型 + +**计算机五大核心组成部分** + +控制器:是整个计算机的中枢神经,其功能是对程序规定的控制信息进行解释,根据其要求进行控制,调度程序、数据、地址,协调计算机各部分工作及内存与外设的访问等。 + +运算器:运算器的功能是对数据进行各种算术运算和逻辑运算,即对数据进行加工处理。 + +存储器:存储器的功能是存储程序、数据和各种信号、命令等信息,并在需要时提供这些信息。 + +输入:输入设备是计算机的重要组成部分,输入设备与输出设备合你为外部设备,简称外设,输入设备的作用是将程序、原始数据、文字、字符、控制命令或现场采集的数据等信息输入到计算机。 + +> 常见的输入设备有键盘、鼠标器、光电输入机、磁带机、磁盘机、光盘机等。 + +输出:输出设备与输入设备同样是计算机的重要组成部分,它把外算机的中间结果或最后结果、机内的各种数据符号及文字或各种控制信号等信息输出出来,微机常用的输出设备有显示终端CRT、打印机、激光印字机、绘图仪及磁带、光盘机等。 + +**计算机结构分成以下 5 个部分:** + +输入设备;输出设备;内存;中央处理器;总线。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-eff4b87b-9091-443c-988b-721e9fd59d2f.png) + +### 内存 + +在冯诺依曼模型中,程序和数据被存储在一个被称作内存的线性排列存储区域。 + +存储的数据单位是一个二进制位,英文是 bit,最小的存储单位叫作字节,也就是 8 位,英文是 byte,每一个字节都对应一个内存地址。 + +内存地址由 0 开始编号,比如第 1 个地址是 0,第 2 个地址是 1, 然后自增排列,最后一个地址是内存中的字节数减 1。 + +我们通常说的内存都是随机存取器,也就是读取任何一个地址数据的速度是一样的,写入任何一个地址数据的速度也是一样的。 + +### CPU + +冯诺依曼模型中 CPU 负责控制和计算,为了方便计算较大的数值,CPU 每次可以计算多个字节的数据。 + +* 如果 CPU 每次可以计算 4 个 byte,那么我们称作 32 位 CPU; + +* 如果 CPU 每次可以计算 8 个 byte,那么我们称作 64 位 CPU。 + +这里的 32 和 64,称作 CPU 的位宽。 + +**为什么 CPU 要这样设计呢?** + +因为一个 byte 最大的表示范围就是 0~255。 + +比如要计算 `20000*50`,就超出了byte 最大的表示范围了。 + +因此,CPU 需要支持多个 byte 一起计算,当然,CPU 位数越大,可以计算的数值就越大,但是在现实生活中不一定需要计算这么大的数值,比如说 32 位 CPU 能计算的最大整数是 4294967295,这已经非常大了。 + +**控制单元和逻辑运算单元** + +CPU 中有一个控制单元专门负责控制 CPU 工作;还有逻辑运算单元专门负责计算。 + +**寄存器** + +CPU 要进行计算,比如最简单的加和两个数字时,因为 CPU 离内存太远,所以需要一种离自己近的存储来存储将要被计算的数字。 + +这种存储就是寄存器,寄存器就在 CPU 里,控制单元和逻辑运算单元非常近,因此速度很快。 + +常见的寄存器种类: + +- 通用寄存器,用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。 +- 程序计数器,用来存储 CPU 要执行下一条指令所在的内存地址,注意不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令的地址。 +- 指令寄存器,用来存放程序计数器指向的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里。 + +#### 多级缓存 + +现代CPU为了提升执行效率,减少CPU与内存的交互(交互影响CPU效率),一般在CPU上集成了多级缓存架构 + +**CPU缓存**即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器 + +由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用,减少CPU的等待时间,提高了系统的效率,具体包括以下几种: + +**L1-Cache** + +L1- 缓存在 CPU 中,相比寄存器,虽然它的位置距离 CPU 核心更远,但造价更低,通常 L1-Cache 大小在几十 Kb 到几百 Kb 不等,读写速度在 2~4 个 CPU 时钟周期。 + +**L2-Cache** + +L2- 缓存也在 CPU 中,位置比 L1- 缓存距离 CPU 核心更远,它的大小比 L1-Cache 更大,具体大小要看 CPU 型号,有 2M 的,也有更小或者更大的,速度在 10~20 个 CPU 周期。 + +**L3-Cache** + +L3- 缓存同样在 CPU 中,位置比 L2- 缓存距离 CPU 核心更远,大小通常比 L2-Cache 更大,读写速度在 20~60 个 CPU 周期。 + +L3 缓存大小也是看型号的,比如 i9 CPU 有 512KB L1 Cache;有 2MB L2 Cache; 有16MB L3 Cache。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-214a4294-df48-4ed4-8f6a-15c93c02037d.png) + +当 CPU 需要内存中某个数据的时候,如果寄存器中有这个数据,我们可以直接使用;如果寄存器中没有这个数据,我们就要先查询 L1 缓存;L1 中没有,再查询 L2 缓存;L2 中没有再查询 L3 缓存;L3 中没有,再去内存中拿。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-936b2476-4704-4928-bfd0-d7417d0f1ab8.png) + + + +**总结:** + +存储器存储空间大小:内存>L3>L2>L1>寄存器; + +存储器速度快慢排序:寄存器>L1>L2>L3>内存; + +#### 安全等级 + +**CPU运行安全等级** + +CPU有4个运行级别,分别为: + +- ring0,ring1,ring2,ring3 + +ring0只给操作系统用,ring3谁都能用。 + +ring0是指CPU的运行级别,是最高级别,ring1次之,ring2更次之…… + +系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。 + +应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。 + +如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。 + +这个过程也称作用户态和内核态的切换。 + +#### 局部性原理 + +在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理 + +**时间局部性(Temporal Locality):** + +如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。 + +比如循环、递归、方法的反复调用等。 + +**空间局部性(Spatial Locality):** + +如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。 + +比如顺序执行的代码、连续创建的两个对象、数组等。 + +#### 程序的执行过程 + +程序实际上是一条一条指令,所以程序的运行过程就是把每一条指令一步一步的执行起来,负责执行指令的就是 CPU 了。 + +**那 CPU 执行程序的过程如下:** + +- 第一步,CPU 读取程序计数器的值,这个值是指令的内存地址,然后 CPU 的控制单元操作地址总线指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过数据总线将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到指令寄存器。 +- 第二步,CPU 分析指令寄存器中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给逻辑运算单元运算;如果是存储类型的指令,则交由控制单元执行; +- 第三步,CPU 执行完指令后,程序计数器的值自增,表示指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此程序计数器的值会自增 4; + +简单总结一下就是,一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。 + +CPU 从程序计数器读取指令、到执行、再到下一条指令,这个过程会不断循环,直到程序执行结束,这个不断循环的过程被称为 **CPU 的指令周期**。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-0e135ca2-26fa-4495-8a4b-3c9c05a4341e.png) + +### 总线 + +CPU 和内存以及其他设备之间,也需要通信,因此我们用一种特殊的设备进行控制,就是总线。 + +- 地址总线,用于指定 CPU 将要操作的内存地址; +- 数据总线,用于读写内存的数据; +- 控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线; + +当 CPU 要读写内存数据的时候,一般需要通过两个总线: + +- 首先要通过地址总线来指定内存的地址; +- 再通过数据总线来传输数据; + +### 输入、输出设备 + +输入设备向计算机输入数据,计算机经过计算,将结果通过输出设备向外界传达。 + +如果输入设备、输出设备想要和 CPU 进行交互,比如说用户按键需要 CPU 响应,这时候就需要用到控制总线。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 基础知识 + +### 中断 + +**中断的类型** + +* 按照中断的触发方分成同步中断和异步中断; + +* 根据中断是否强制触发分成可屏蔽中断和不可屏蔽中断。 + +中断可以由 CPU 指令直接触发,这种主动触发的中断,叫作同步中断。 + +> 同步中断有几种情况。 + +* 比如系统调用,需要从用户态切换内核态,这种情况需要程序触发一个中断,叫作陷阱(Trap),中断触发后需要继续执行系统调用。 + +* 还有一种同步中断情况是错误(Fault),通常是因为检测到某种错误,需要触发一个中断,中断响应结束后,会重新执行触发错误的地方,比如后面我们要学习的缺页中断。 + +* 最后还有一种情况是程序的异常,这种情况和 Trap 类似,用于实现程序抛出的异常。 + +另一部分中断不是由 CPU 直接触发,是因为需要响应外部的通知,比如响应键盘、鼠标等设备而触发的中断,这种中断我们称为异步中断。 + +CPU 通常都支持设置一个中断屏蔽位(一个寄存器),设置为 1 之后 CPU 暂时就不再响应中断。 + +对于键盘鼠标输入,比如陷阱、错误、异常等情况,会被临时屏蔽。 + +但是对于一些特别重要的中断,比如 CPU 故障导致的掉电中断,还是会正常触发。 + +**可以被屏蔽的中断我们称为可屏蔽中断,多数中断都是可屏蔽中断。** + +### 内核态和用户态 + +**什么是用户态和内核态** + +Kernel 运行在超级权限模式下,所以拥有很高的权限。 + +按照权限管理的原则,多数应用程序应该运行在最小权限下。 + +因此,很多操作系统,将内存分成了两个区域: + +* 内核空间(Kernal Space),这个空间只有内核程序可以访问; + +* 用户空间(User Space),这部分内存专门给应用程序使用。 + +用户空间中的代码被限制了只能使用一个局部的内存空间,我们说这些程序在用户态 执行。 + +内核空间中的代码可以访问所有内存,我们称这些程序在内核态 执行。 + +> 按照级别分: + +当程序运行在0级特权级上时,就可以称之为运行在内核态 + +当程序运行在3级特权级上时,就可以称之为运行在用户态 + +运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。 + +当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态(比如操作硬件) + +**这两种状态的主要差别** + +处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的 + +处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。 + +**为什么要有用户态和内核态** + +由于需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者获取外围设备的数据,并发送到网络 + +**用户态与内核态的切换** + +所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据,或者从键盘获取输入等,而唯一可以做这些事情的就是操作系统,所以此时程序就需要先操作系统请求以程序的名义来执行这些操作 + +**用户态和内核态的转换** + +> 系统调用 + +用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如fork()实际上就是执行了一个创建新进程的系统调用 + +而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断 + +**举例:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-29b6f34f-a8b3-48ec-8e5c-aea1036ea16a.png) + +如上图所示:内核程序执行在内核态(Kernal Mode),用户程序执行在用户态(User Mode)。 + +当发生系统调用时,用户态的程序发起系统调用,因为系统调用中牵扯特权指令,用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。 + +发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序,内核程序开始执行,也就是开始处理系统调用。 + +内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。 + +> 异常 + +当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常 + +> 外围设备的中断 + +当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换 + +比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 线程 + +线程:系统分配处理器时间资源的基本单元,是程序执行的最小单位 + +线程可以看做轻量级的进程,共享内存空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。 + +在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行) + +进程可以通过 API 创建用户态的线程,也可以通过系统调用创建内核态的线程。 + +### 用户态线程 + +用户态线程也称作用户级线程,操作系统内核并不知道它的存在,它完全是在用户空间中创建。 + +用户级线程有很多优势,比如: + +* 管理开销小:创建、销毁不需要系统调用。 + +* 切换成本低:用户空间程序可以自己维护,不需要走操作系统调度。 + +但是这种线程也有很多的缺点: + +* 与内核协作成本高:比如这种线程完全是用户空间程序在管理,当它进行 I/O 的时候,无法利用到内核的优势,需要频繁进行用户态到内核态的切换。 + +* 线程间协作成本高:设想两个线程需要通信,通信需要 I/O,I/O 需要系统调用,因此用户态线程需要额外的系统调用成本。 + +* 无法利用多核优势:比如操作系统调度的仍然是这个线程所属的进程,所以无论每次一个进程有多少用户态的线程,都只能并发执行一个线程,因此一个进程的多个线程无法利用多核的优势。 + +操作系统无法针对线程调度进行优化:当一个进程的一个用户态线程阻塞(Block)了,操作系统无法及时发现和处理阻塞问题,它不会更换执行其他线程,从而造成资源浪费。 + +### 内核态线程 + +内核态线程也称作内核级线程(Kernel Level Thread),这种线程执行在内核态,可以通过系统调用创造一个内核级线程。 + +内核级线程有很多优势: + +* 可以利用多核 CPU 优势:内核拥有较高权限,因此可以在多个 CPU 核心上执行内核线程。 + +* 操作系统级优化:内核中的线程操作 I/O 不需要进行系统调用;一个内核线程阻塞了,可以立即让另一个执行。 + +当然内核线程也有一些缺点: + +* 创建成本高:创建的时候需要系统调用,也就是切换到内核态。 + +* 扩展性差:由一个内核程序管理,不可能数量太多。 + +* 切换成本较高:切换的时候,也同样存在需要内核操作,需要切换内核态。 + +**用户态线程和内核态线程之间的映射关系** + +如果有一个用户态的进程,它下面有多个线程,如果这个进程想要执行下面的某一个线程,应该如何做呢? + +> 这时,比较常见的一种方式,就是将需要执行的程序,让一个内核线程去执行。 + +毕竟,内核线程是真正的线程,因为它会分配到 CPU 的执行资源。 + +如果一个进程所有的线程都要自己调度,相当于在进程的主线程中实现分时算法调度每一个线程,也就是所有线程都用操作系统分配给主线程的时间片段执行。 + +> 这种做法,相当于操作系统调度进程的主线程;进程的主线程进行二级调度,调度自己内部的线程。 + +这样操作劣势非常明显,比如无法利用多核优势,每个线程调度分配到的时间较少,而且这种线程在阻塞场景下会直接交出整个进程的执行权限。 + +由此可见,用户态线程创建成本低,问题明显,不可以利用多核。 + +内核态线程,创建成本高,可以利用多核,切换速度慢。 + +因此通常我们会在内核中预先创建一些线程,并反复利用这些线程。 + +### 协程 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-586071a1-3d5f-4873-963f-d47b8aeea594.png) + +协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。 + +这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。 + +**子程序** + +或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。 + +所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。 + +子程序调用总是一个入口,一次返回,调用顺序是明确的。 + +**协程的特点在于是一个线程执行,那和多线程比,协程有何优势?** + +* 极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显; +* 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。 + +### 线程安全 + +如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。 + +如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 进程 + +在系统中正在运行的一个应用程序;程序一旦运行就是进程;是资源分配的最小单位。 + +在操作系统中能同时运行多个进程; + +开机的时候,磁盘的内核镜像被导入内存作为一个执行副本,成为内核进程。 + +进程可以分成**用户态进程和内核态进程**两类,用户态进程通常是应用程序的副本,内核态进程就是内核本身的进程。 + +如果用户态进程需要申请资源,比如内存,可以通过系统调用向内核申请。 + +每个进程都有独立的内存空间,存放代码和数据段等,程序之间的切换会有较大的开销; + +**分时和调度** + +每个进程在执行时都会获得操作系统分配的一个时间片段,如果超出这个时间,就会轮到下一个进程(线程)执行。 + +> 注意,现代操作系统都是直接调度线程,不会调度进程。 + +**分配时间片段** + +如下图所示,进程 1 需要 2 个时间片段,进程 2 只有 1 个时间片段,进程 3 需要 3 个时间片段。 + +因此当进程 1 执行到一半时,会先挂起,然后进程 2 开始执行;进程 2 一次可以执行完,然后进程 3 开始执行,不过进程 3 一次执行不完,在执行了 1 个时间片段后,进程 1 开始执行;就这样如此周而复始,这个就是分时技术。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-606eb7bd-e9e1-457a-bf61-66b152c5ada5.png) + +### 创建进程 + +用户想要创建一个进程,最直接的方法就是从命令行执行一个程序,或者双击打开一个应用,但对于程序员而言,显然需要更好的设计。 + +首先,应该有 API 打开应用,比如可以通过函数打开某个应用; + +另一方面,如果程序员希望执行完一段代价昂贵的初始化过程后,将当前程序的状态复制好几份,变成一个个单独执行的进程,那么操作系统提供了 fork 指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-85a86f1b-eeff-401e-8bfe-19e5509e2a53.png) + +也就是说,每次 fork 会多创造一个克隆的进程,这个克隆的进程,所有状态都和原来的进程一样,但是会有自己的地址空间。 + +如果要创造 2 个克隆进程,就要 fork 两次。 + +> 那如果我就是想启动一个新的程序呢? + +操作系统提供了启动新程序的 API。 + +如果我就是想用一个新进程执行一小段程序,比如说每次服务端收到客户端的请求时,我都想用一个进程去处理这个请求。 + +如果是这种情况,建议你不要单独启动进程,而是使用线程。 + +因为进程的创建成本实在太高了,因此不建议用来做这样的事情:要创建条目、要分配内存,特别是还要在内存中形成一个个段,分成不同的区域。所以通常,我们更倾向于多创建线程。 + +不同程序语言会自己提供创建线程的 API,比如 Java 有 Thread 类;go 有 go-routine(注意不是协程,是线程)。 + +### 进程状态 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-5f202fa5-0922-4b41-b4f2-f8a9af37a176.png) + +**创建状态** + +进程由创建而产生,创建进程是一个非常复杂的过程,一般需要通过多个步骤才能完成:如首先由进程申请一个空白的进程控制块(PCB),并向PCB中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源;最后,把该进程转入就绪状态并插入到就绪队列中 + +**就绪状态** + +这是指进程已经准备好运行的状态,即进程已分配到除CPU以外所有的必要资源后,只要再获得CPU,便可立即执行,如果系统中有许多处于就绪状态的进程,通常将它们按照一定的策略排成一个队列,该队列称为就绪队列,有执行资格,没有执行权的进程 + +**运行状态** + +这里指进程已经获取CPU,其进程处于正在执行的状态。对任何一个时刻而言,在单处理机的系统中,只有一个进程处于执行状态而在多处理机系统中,有多个进程处于执行状态,既有执行资格,又有执行权的进程 + +**阻塞状态** + +这里是指正在执行的进程由于发生某事件(如I/O请求、申请缓冲区失败等)暂时无法继续执行的状态,即进程执行受到阻塞,此时引起进程调度,操作系统把处理机分配给另外一个就绪的进程,而让受阻的进程处于暂停的状态,一般将这个暂停状态称为阻塞状态 + +**终止状态** + +### 进程间通信IPC + +每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信 + +**管道/匿名管道** + +管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。 + +* 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程); + +* 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。 + +* 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出,写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。 + +**有名管道(FIFO)** + +匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。 + +为了克服这个缺点,提出了有名管道(FIFO)。 + +有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。 + +**信号** + +信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。 + +如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。 + +如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。 + +**消息队列** + +消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。 + +与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。 + +另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达 + +**共享内存** + +使得多个进程可以直接读写同一块内存空间,是最快的可用IPC形式,是针对其他通信机制运行效率较低而设计的。 + +为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间,进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。 + +由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。 + +共享内存示意图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-8644f82a-eda6-4abf-b86a-13c02e7af5fd.png) + +一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。 + +**信号量** + +信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。 + +为了获得共享资源,进程需要执行下列操作: + +1. 创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。 + +2. 等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞,也称为P操作。 +3. 挂出一个信号量:该操作将信号量的值加1,也称为V操作。 + +**套接字(Socket)** + +套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。 + +### 信号 + +信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。 + +也可以简单理解为信号是某种形式上的软中断 + +可运行`kill -l`查看Linux支持的信号列表: + +``` +kill -l + 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP + 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 +11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM +16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP +21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ +26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR +31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 +38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 +43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 +48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 +53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 +58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 +63) SIGRTMAX-1 64) SIGRTMAX +``` + +**几个常用的信号:** + +| 信号 | 描述 | +| ------- | ------------------------------------------------------------ | +| SIGHUP | 当用户退出终端时,由该终端开启的所有进程都会接收到这个信号,默认动作为终止进程。 | +| SIGINT | 程序终止(interrupt)信号, 在用户键入INTR字符(通常是`Ctrl+C`)时发出,用于通知前台进程组终止进程。 | +| SIGQUIT | 和`SIGINT`类似, 但由QUIT字符(通常是`Ctrl+\`)来控制,进程在因收到`SIGQUIT`退出时会产生`core`文件, 在这个意义上类似于一个程序错误信号。 | +| SIGKILL | 用来立即结束程序的运行,本信号不能被阻塞、处理和忽略。 | +| SIGTERM | 程序结束(terminate)信号, 与`SIGKILL`不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。 | +| SIGSTOP | 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行,本信号不能被阻塞, 处理或忽略 | + +### 进程同步 + +**临界区** + +通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问 + +优点:保证在某一时刻只有一个线程能访问数据的简便办法 + +缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程 + +**互斥量** + +为协调共同对一个共享资源的单独访问而设计的 + +互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限 + +优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享 + +**信号量** + +为控制一个具有有限数量用户资源而设计,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目,互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了 + +信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作 + +- **down** : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0; +- **up** :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。 + +down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。 + +如果信号量的取值只能为 0 或者 1,那么就成为了 **互斥量(Mutex)** ,0 表示临界区已经加锁,1 表示临界区解锁。 + +**事件** + +用来通知线程有一些事件已发生,从而启动后继任务的开始 + +优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作 + +**管程** + +管程有一个重要特性:在一个时刻只能有一个进程使用管程。 + +进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。 + +管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。 + +对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。 + +signal() 操作用于唤醒被阻塞的进程。 + +使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。 + +### 上下文切换 + +对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。 + +上下文切换(Context Switch)是一种将CPU资源从一个进程分配给另一个进程的机制。 + +从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。 + +**在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。** + +### 进程调度算法 + +**先来先服务调度算法** + +该算法既可用于作业调度,也可用于进程调度,当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列 + +**短作业优先调度算法** + +从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行 + +**时间片轮转法** + +每次调度时,把CPU分配给队首进程,并令其执行一个时间片,时间片的大小从几ms到几百ms,当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾 + +然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片,这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间 + +**最短剩余时间优先** + +最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度,当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。 + +如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 + +**多级反馈队列调度算法**: + +前面介绍的几种进程调度的算法都有一定的局限性,如**短进程优先的调度算法,仅照顾了短进程而忽略了长进程**,多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业迅速完成,因而它是目前**被公认的一种较好的进程调度算法**,UNIX 操作系统采取的便是这种调度算法。 + +> 举例: + +多级队列,就是多个队列执行调度,先考虑最简单的两级模型 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-bcc4bc31-75b9-43f2-9f16-7a0e419e5615.png) + +上图中设计了两个优先级不同的队列,从下到上优先级上升,上层队列调度紧急任务,下层队列调度普通任务。 + +只要上层队列有任务,下层队列就会让出执行权限。 + +低优先级队列可以考虑抢占 + 优先级队列的方式实现,这样每次执行一个时间片段就可以判断一下高优先级的队列中是否有任务。 + +高优先级队列可以考虑用非抢占(每个任务执行完才执行下一个)+ 优先级队列实现,这样紧急任务优先级有个区分,如果遇到十万火急的情况,就可以优先处理这个任务。 + +上面这个模型虽然解决了任务间的优先级问题,但是还是没有解决短任务先行的问题,可以考虑再增加一些队列,让级别更多。 + +> 比如下图这个模型: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-85f35508-c541-483b-9f34-a54a0c619fff.png) + +紧急任务仍然走高优队列,非抢占执行。 + +普通任务先放到优先级仅次于高优任务的队列中,并且只分配很小的时间片;如果没有执行完成,说明任务不是很短,就将任务下调一层。 + +下面一层,最低优先级的队列中时间片很大,长任务就有更大的时间片可以用。 + +通过这种方式,短任务会在更高优先级的队列中执行完成,长任务优先级会下调,也就类似实现了最短作业优先的问题。 + +实际操作中,可以有 n 层,一层层把大任务筛选出来,最长的任务,放到最闲的时间去执行,要知道,大部分时间 CPU 不是满负荷的。 + +**优先级调度** + +为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推,具有相同优先级的进程以 FCFS 方式执行,可以根据内存要求,时间要求或任何其他资源要求来确定优先级。 + +### 守护进程 + +守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示,并且进程也不会被任何终端所产生的终端信息所打断。 + +守护进程一般的生命周期是系统启动到系统停止运行。 + +Linux系统中有很多的守护进程,最典型的就是我们经常看到的服务进程。 + +当然,我们也经常会利用守护进程来完成很多的系统或者自动化任务。 + +### 孤儿进程 + +父进程早于子进程退出时候子进程还在运行,子进程会成为孤儿进程,Linux会对孤儿进程的处理,把孤儿进程的父进程设为进程号为1的进程,也就是由init进程来托管,init进程负责子进程退出后的善后清理工作 + +### 僵尸进程 + +子进程执行完毕时发现父进程未退出,会向父进程发送 SIGCHLD 信号,但父进程没有使用 wait/waitpid 或其他方式处理 SIGCHLD 信号来回收子进程,子进程变成为了对系统有害的僵尸进程 + +子进程退出后留下的进程信息没有被收集,会导致占用的进程控制块PCB不被释放,形成僵尸进程,进程已经死去,但是进程资源没有被释放掉 + +**问题及危害** + +如果系统中存在大量的僵尸进程,他们的进程号就会一直被占用,但是系统所能使用的进程号是有限的,系统将因为没有可用的进程号而导致系统不能产生新的进程 + +任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理,这是每个子进程在结束时都要经过的阶段,如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是Z。 + +如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态 + +产生僵尸进程的元凶其实是他们的父进程,杀掉父进程,僵尸进程就变为了孤儿进程,便可以转交给 init 进程回收处理 + +### 死锁 + +**产生原因** + +系统资源的竞争:系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。 + +进程运行推进顺序不合适:进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。 + +**发生死锁的四个必要条件** + +互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有,此时若有其他进程请求该资源,则请求进程只能等待 + +请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求时,该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放 + +不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放) + +循环等待条件: 若干进程间形成首尾相接循环等待资源的关系 + +这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁 + +**只要我们破坏其中一个,就可以成功避免死锁的发生** + +其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥 + +1. 对于占用且等待这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。 +2. 对于不可抢占这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。 +3. 对于循环等待这个条件,可以靠按序申请资源来预防,所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。 + +**处理方法** + +主要有以下四种方法: + +- 鸵鸟策略 +- 死锁检测与死锁恢复 +- 死锁预防,破坏4个必要条件 +- 死锁避免,银行家算法 + +**鸵鸟策略** + +把头埋在沙子里,假装根本没发生问题。 + +因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。 + +当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。 + +**死锁检测** + +不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。 + +1. 每种类型一个资源的死锁检测 + +2. 每种类型多个资源的死锁检测 + +**死锁恢复** + +- 利用抢占恢复 +- 利用回滚恢复 +- 通过杀死进程恢复 + +#### 哲学家进餐问题 + +五个哲学家围着一张圆桌,每个哲学家面前放着食物。 + +哲学家的生活有两种交替活动:吃饭以及思考。 + +当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。 + +如果所有哲学家同时拿起左手边的筷子,那么所有哲学家都在等待其它哲学家吃完并释放自己手中的筷子,导致死锁。 + +哲学家进餐问题可看作是并发进程并发执行时处理共享资源的一个有代表性的问题。 + +**为了防止死锁的发生,可以设置两个条件:** + +- 必须同时拿起左右两根筷子; +- 只有在两个邻居都没有进餐的情况下才允许进餐。 + +#### 银行家算法 + +银行家算法的命名是它可以用了银行系统,当不能满足所有客户的需求时,银行绝不会分配其资金。 + +当新进程进入系统时,它必须说明其可能需要的每种类型资源实例的最大数量这一数量不可以超过系统资源的总和。 + +当用户申请一组资源时,系统必须确定这些资源的分配是否处于安全状态,如何安全,则分配,如果不安全,那么进程必须等待指导某个其他进程释放足够资源为止。 + +**安全状态** + +在避免死锁的方法中,允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性,若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,令进程等待 + +因此,避免死锁的实质在于:系统在进行资源分配时,如何使系统不进入不安全状态 + +### Fork函数 + +`fork`函数用于创建一个与当前进程一样的子进程,所创建的子进程将复制父进程的代码段、数据段、BSS段、堆、栈等所有用户空间信息,在内核中操作系统会重新为其申请一个子进程执行的位置。 + +`fork`系统调用会通过复制一个现有进程来创建一个全新的进程,新进程被存放在一个叫做任务队列的双向循环链表中,链表中的每一项都是类型为`task_struct`的进程控制块`PCB`的结构。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-ee3d0c6b-9d93-4f9d-95f3-86e7d195a8f8.png) + +每个进程都由独特换不相同的进程标识符(PID),通过`getpid()`函数可获取当前进程的进程标识符,通过`getppid()`函数可获得父进程的进程标识符。 + +一个现有的进程可通过调用`fork`函数创建一个新进程,由`fork`创建的新进程称为子进程`child process`,`fork`函数被调用一次但返回两次,两次返回的唯一区别是子进程中返回0而父进程中返回子进程ID。 + +**为什么`fork`会返回两次呢?** + +因为复制时会复制父进程的堆栈段,所以两个进程都停留在`fork`函数中等待返回,因此会返回两次,一个是在父进程中返回,一次是在子进程中返回,两次返回值是不一样的。 + +- 在父进程中将返回新建子进程的进程ID +- 在子进程中将返回0 +- 若出现错误则返回一个负数 + +因此可以通过`fork`的返回值来判断当前进程是子进程还是父进程。 + +**fork执行执行流程** + +当进程调用`fork`后控制转入内核,内核将会做4件事儿: + +1. 分配新的内存块和内核数据结构给子进程 +2. 将父进程部分数据结构内容(数据空间、堆栈等)拷贝到子进程 +3. 添加子进程到系统进程列表中 +4. `fork`返回开始调度器调度 + +**为什么`pid`在父子进程中不同呢?** + +其实就相当于链表,进程形成了链表,父进程的`pid`指向子进程的进程ID,因此子进程没有子进程,所以PID为0,这里的`pid`相当于链表中的指针。 + +## 设备管理 + +### 磁盘调度算法 + +读写一个磁盘块的时间的影响因素有: + +- 旋转时间 +- 寻道时间实际的数据传输时间 + +其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。 + +> 先来先服务 FCFS, First Come First Served + +按照磁盘请求的顺序进行调度,优点是公平和简单,缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。 + +> 最短寻道时间优先,SSTF, Shortest Seek Time First + +优先调度与当前磁头所在磁道距离最近的磁道, 虽然平均寻道时间比较低,但是不够公平,如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的 磁道请求会一直等待下去,也就是出现饥饿现象,具体来说,两边的磁道请求更容易出现饥饿现象。 + +> 电梯算法,SCAN + +电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向, 电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘 请求,然后改变方向,因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 内存管理 + +**逻辑地址和物理地址** + +我们编程一般只有可能和逻辑地址打交道,比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。 + +物理地址指的是真实物理内存中地址,更具体一点来说就是内存地址寄存器中的地址,物理地址是内存单元真正的地址。 + +编译时只需确定变量x存放的相对地址是100 ( 也就是说相对于进程在内存中的起始地址而言的地址)。 + +CPU想要找到x在内存中的实际存放位置,只需要用进程的起始地址+100即可。 + +相对地址又称逻辑地址,绝对地址又称物理地址。 + +**内存管理有哪几种方式** + +1. **块式管理**:将内存分为几个固定大小的块,每个块中只包含一个进程,如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了,这些在每个块中未被利用的空间,我们称之为碎片。 +2. **页式管理**:把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片,页式管理通过页表对应逻辑地址和物理地址。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-2d1e6109-5c99-4ab8-b9b4-00c851386d65.png) + +1. **段式管理**: 页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义, 段式管理把主存分为一段段的,每一段的空间又要比一页的空间小很多 ,段式管理通过段表对应逻辑地址和物理地址。 +2. **段页式管理机制:**段页式管理机制结合了段式管理和页式管理的优点,简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说**段页式管理机制**中段与段之间以及段的内部的都是离散的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-06d985c8-7c5a-46eb-8f04-e7a010a90f40.png) + +### 虚拟地址 + +现代处理器使用的是一种称为**虚拟寻址(Virtual Addressing)**的寻址方式 + +**使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。** + +实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为**内存管理单元(Memory Management Unit, MMU)**的硬件 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-ad8515bb-ce0c-43b3-b9ff-9d4d5fa0994e.png) + +**为什么要有虚拟地址空间** + +没有虚拟地址空间的时候,**程序都是直接访问和操作的都是物理内存**。 + +但是这样有什么问题? + +1. 用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易破坏操作系统,造成操作系统崩溃。 +2. 想要同时运行多个程序特别困难,比如你想同时运行一个微信和一个 QQ 音乐都不行,为什么呢?举个简单的例子:微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就造成了微信这个程序就会崩溃。 + +**通过虚拟地址访问内存有以下优势:** + +- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。 +- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。 +- 不同进程使用的虚拟地址彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。 + +**MMU如何把虚拟地址翻译成物理地址的** + +对于每个程序,内存管理单元MMU都为其保存一个页表,该页表中存放的是虚拟页面到物理页面的映射。 + +每当为一个虚拟页面寻找到一个物理页面之后,就在页表里增加一条记录来保留该映射关系,当然,随着虚拟页面进出物理内存,页表的内容也会不断更新变化。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-169790fd-8ef1-400f-a4a4-e291a729d2cb.png) + +### 虚拟内存 + +很多时候我们使用点开了很多占内存的软件,这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存 + +通过 **虚拟内存** 可以让程序可以拥有超过系统物理内存大小的可用内存空间。 + +另外,虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间),这样会更加有效地管理内存并减少出错。 + +**虚拟内存**是计算机系统内存管理的一种技术,我们可以手动设置自己电脑的虚拟内存 + +**虚拟内存的重要意义是它定义了一个连续的虚拟地址空间**,并且 **把内存扩展到硬盘空间** + +**虚拟内存的实现有以下三种方式:** + +1. **请求分页存储管理** :请求分页是目前最常用的一种实现虚拟存储器的方法,请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行,假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。 +2. **请求分段存储管理** :请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段。 +3. **请求段页式存储管理** + +不管是上面那种实现方式,我们一般都需要: + +> 一定容量的内存和外存:在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存,然后程序就可以执行了; + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-d2104a4e-cb28-4d90-b8f1-92cb15b0bb31.png) + + + +### 缺页中断 + +如果**需执行的指令或访问的数据尚未在内存**(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段**调入到内存**,然后继续执行程序; + +在分页系统中,一个虚拟页面既有可能在物理内存,也有可能保存在磁盘上。 + +如果CPU发出的虚拟地址对应的页面不在物理内存,就将产生一个缺页中断,而缺页中断服务程序负责将需要的虚拟页面找到并加载到内存。 + +缺页中断的处理步骤如下,省略了中间很多的步骤,只保留最核心的几个步骤: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-5101231b-d8da-418e-b60c-574bcfa8b579.png) + +### 页面置换算法 + +当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。 + +用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则 + +- **OPT 页面置换算法(最佳页面置换算法)** :该置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率,但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现,一般作为衡量其他置换算法的方法。 +- **FIFO(First In First Out) 页面置换算法(先进先出页面置换算法)** : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。 + +- **LRU (Least Currently Used)页面置换算法(最近最久未使用页面置换算法)** :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。 +- **LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法)** : 该置换算法选择在之前时期使用最少的页面作为淘汰页。 + +### 局部性原理 + +局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装入部分程序到内存就开始运行。 + +局部性原理表现在以下两个方面: + +1. **时间局部性** :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问,产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。 +2. **空间局部性** :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。 + +时间局部性是通过将近来使用的指令和数据保存到**高速缓存存储器**中,并使用高速缓存的层次结构实现。 + +空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。 + +### 页表 + +操作系统将虚拟内存分块,每个小块称为一个页(Page);真实内存也需要分块,每个小块我们称为一个 Frame。 + +Page 到 Frame 的映射,需要一种叫作页表的结构。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-d3ce7f2b-275c-422c-8793-f3e0e1894ecc.png) + +上图展示了 Page、Frame 和页表 (PageTable)三者之间的关系。 + +Page 大小和 Frame 大小通常相等,页表中记录的某个 Page 对应的 Frame 编号。 + +页表也需要存储空间,比如虚拟内存大小为 10G, Page 大小是 4K,那么需要 10G/4K = 2621440 个条目。 + +如果每个条目是 64bit,那么一共需要 20480K = 20M 页表,操作系统在内存中划分出小块区域给页表,并负责维护页表。 + +**页表维护了虚拟地址到真实地址的映射。** + +每次程序使用内存时,需要把虚拟内存地址换算成物理内存地址,换算过程分为以下 3 个步骤: + +* 通过虚拟地址计算 Page 编号; + +* 查页表,根据 Page 编号,找到 Frame 编号; + +* 将虚拟地址换算成物理地址。 + +#### 多级页表 + +引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中 + +**一级页表:** + +假如物理内存中一共有1048576个页,那么页表就需要总共就是`1048576 * 4B = 4M`。 + +也就是说我需要4M连续的内存来存放这个页表,也就是一级页表。 + +随着虚拟地址空间的增大,存放页表所需要的连续空间也会增大,在操作系统内存紧张或者内存碎片较多时,这无疑会带来额外的开销。 + +页表寻址是用寄存器来确定一级页表地址的,所以一级页表的地址必须指向确定的物理页,否则就会出现错误,所以如果用一级页表的话,就必须把全部的页表都加载进去。 + +**二级页表:** + +而使用二级页表的话,只需要加载一个页目录表(一级页表),大小为4K,可以管理1024个二级页表。 + +可能你会有疑问,这1024个二级页表也是需要内存空间的,这下反而需要4MB+4KB的内存,反而更多了。 + +其实二级页表并不是一定要存在内存中的,内存中只需要一个一级页表地址存在存器即可,二级页表可以使用缺页中断从外存移入内存。 + +**多级页表属于时间换空间的典型场景** + +### 快表 + +为了解决虚拟地址到物理地址的转换速度,操作系统在**页表方案**基础之上引入了**快表**来加速虚拟地址到物理地址的转换 + +我们可以把快表理解为一种特殊的**高速缓冲存储器(Cache)**,其中的内容是页表的一部分或者全部内容,作为页表的 Cache,它的作用与页表相似,但是提高了访问速率,由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存,有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。 + +**使用快表之后的地址转换流程是这样的:** + +1. 根据虚拟地址中的页号查快表; +2. 如果该页在快表中,直接从快表中读取相应的物理地址; +3. 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中; +4. 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-a9551753-2578-474e-a452-192d01382d22.png) + +### 内存管理单元 + +在 CPU 中一个小型的设备——内存管理单元(MMU) + +![](https://img-blog.csdnimg.cn/2da3a2f130cf415cb0b42c19fda70f30.png"> + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-51d4172c-068b-46b2-a7bc-fb4a6d39955b.png) + +当 CPU 需要执行一条指令时,如果指令中涉及内存读写操作,CPU 会把虚拟地址给 MMU,MMU 自动完成虚拟地址到真实地址的计算;然后,MMU 连接了地址总线,帮助 CPU 操作真实地址。 + +在不同 CPU 的 MMU 可能是不同的,因此这里会遇到很多跨平台的问题。 + +解决跨平台问题不但有繁重的工作量,更需要高超的编程技巧。 + +### 动态分区分配算法 + +内存分配算法,大体来说分为:**连续式分配 与 非连续式分配** + +连续式分配就是把所以要执行的程序 **完整的,有序的** 存入内存,连续式分配又可以分为**固定分区分配 和 动态分区分配** + +非连续式分配就是把要执行的程序按照一定规则进行拆分,显然这样更有效率,现在的操作系统通常也都是采用这种方式分配内存 + +所谓动态分区分配,就是指**内存在初始时不会划分区域,而是会在进程装入时,根据所要装入的进程大小动态地对内存空间进行划分,以提高内存空间利用率,降低碎片的大小** + +动态分区分配算法有以下四种: + +> 首次适应算法(First Fit) + +空闲分区以地址递增的次序链接,分配内存时顺序查找,找到大小满足要求的第一个空闲分区就进行分配 + +![](https://img-blog.csdnimg.cn/e5cf8456bf9941758f6a1bd2ba1c351a.png"> + +> 邻近适应算法(Next Fit) + +又称循环首次适应法,由首次适应法演变而成,不同之处是分配内存时从上一次查找结束的位置开始继续查找 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-a072428d-1d01-4c93-b5bb-8d2f9721d2ba.png) + +> 最佳适应算法(Best Fit) + +空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区就进行分配 + +![](https://img-blog.csdnimg.cn/585a5ba3e4ad4eaeae77ebd1d8b02b32.png"> + +> 最坏适应算法(Next Fit) + +又称最大适应算法,空闲分区以容量递减的次序链接,找到第一个能满足要求的空闲分区(也就是最大的分区)就进行分配 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-4673dda6-3940-4a15-ba09-6acfe095ff62.png) + +**总结** + +首次适应不仅最简单,通常也是最好最快,不过首次适应算法会使得内存低地址部分出现很多小的空闲分区,而每次查找都要经过这些分区,因此也增加了查找的开销。 + +邻近算法试图解决这个问题,但实际上,它常常会导致在内存的末尾分配空间分裂成小的碎片,它通常比首次适应算法结果要差。 + +最佳适应算法导致大量碎片,最坏适应算法导致没有大的空间。 + +### 内存覆盖 + +覆盖与交换技术是在程序用来扩充内存的两种方法。 + +早期的计算机系统中,主存容量很小,虽然主存中仅存放一道用户程序,但是存储空间放不下用户进程的现象也经常发生,这一矛盾可以用覆盖技术来解决。 + +**覆盖的基本思想是:** + +由于程序运行时并非任何时候都要访问程序及数据的各个部分(尤其是大程序),因此可以把用户空间分成一个固定区和若干个覆盖区。 + +将经常活跃的部分放在固定区,其余部分按调用关系分段。 + +首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统再将其调入覆盖区,替换覆盖区中原有的段。 + +覆盖技术的特点是打破了必须将一个进程的全部信息装入主存后才能运行的限制,但当同时运行程序的代码量大于主存时仍不能运行。 + +### 内存交换 + +**交换的基本思想** + +把处于等待状态(或在CPU调度原则下被剥夺运行权利)的程序从内存移到辅存,把内存空间腾出来,这一过程又叫换出; + +把准备好竞争CPU运行的程序从辅存移到内存,这一过程又称为换入。 + +> 例如,有一个CPU釆用时间片轮转调度算法的多道程序环境。 + +时间片到,内存管理器将刚刚执行过的进程换出,将另一进程换入到刚刚释放的内存空间中。 + +同时,CPU调度器可以将时间片分配给其他已在内存中的进程。 + +每个进程用完时间片都与另一进程交换。 + +理想情况下,内存管理器的交换过程速度足够快,总有进程在内存中可以执行。 + +> 交换技术主要是在不同进程(或作业)之间进行,而覆盖则用于同一个程序或进程中。 + +由于覆盖技术要求给出程序段之间的覆盖结构,使得其对用户和程序员不透明,所以对于主存无法存放用户程序的矛盾 + +现代操作系统是通过虚拟内存技术来解决的,覆盖技术则已成为历史;而交换技术在现代操作系统中仍具有较强的生命力。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + +## 常见面试题 + +**进程、线程的区别** + +操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-6a48d3af-0342-4529-a45e-3ff739d8b22c.png) + +调度:线程作为CPU调度和分配的基本单位,进程作为拥有资源的基本单位; + +并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行; + +> 拥有资源: + +进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。 + +进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler等; + +线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集等; + +> 系统开销: + +在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。 + +但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。 + +线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。 + +**一个进程可以创建多少线程** + +理论上,一个进程可用虚拟空间是2G,默认情况下,线程的栈的大小是1MB,所以理论上最多只能创建2048个线程。 + +如果要创建多于2048的话,必须修改编译器的设置。 + +在一般情况下,你不需要那么多的线程,过多的线程将会导致大量的时间浪费在线程切换上,给程序运行效率带来负面影响。 + +**外中断和异常有什么区别** + +外中断是指由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求,此外还有时钟中断、控制台中断等。 + +而异常时由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。 + +**解决Hash冲突四种方法** + +开放定址法 + +- 开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。 + +链地址法 + +- 将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。 + +再哈希法 + +- 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。 + +建立公共溢出区 + +- 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。 + +**分页机制和分段机制有哪些共同点和区别** + +共同点 + +- 分页机制和分段机制都是为了提高内存利用率,较少内存碎片。 +- 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。 + +区别 + +- 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。 +- 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。 +- 分页是一维地址空间,分段是二维的。 + +**介绍一下几种典型的锁** + +> 读写锁 + +- 可以同时进行多个读 +- 写者必须互斥(只允许一个写者写,也不能读者写者同时进行) +- 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者) + +> 互斥锁 + +一次只能一个线程拥有互斥锁,其他线程只有等待 + +互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。 + +互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁 + +> 条件变量 + +互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。 + +而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。 + +当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。 + +一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。 + +总的来说**互斥锁是线程间互斥的机制,条件变量则是同步机制。** + +> 自旋锁 + +如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。 + +如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。 + +虽然它的效率比互斥锁高,但是它也有些不足之处: + +- 自旋锁一直占用CPU,在未获得锁的情况下,一直进行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,无疑会使CPU效率降低。 +- 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。 + +**如何让进程后台运行** + +1.命令后面加上&即可,实际上,这样是将命令放入到一个作业队列中了 + +2.ctrl + z 挂起进程,使用jobs查看序号,在使用bg %序号后台运行进程 + +3.nohup + &,将标准输出和标准错误缺省会被重定向到 `nohup.out` 文件中,忽略所有挂断(SIGHUP)信号 + +``` +nohup ping www.ibm.com & +``` + +4.运行指令前面 + setsid,使其父进程变成init进程,不受SIGHUP信号的影响 + +``` +[root@pvcent107 ~]## setsid ping www.ibm.com +[root@pvcent107 ~]## ps -ef |grep www.ibm.com +root 31094 1 0 07:28 ? 00:00:00 ping www.ibm.com +root 31102 29217 0 07:29 pts/4 00:00:00 grep www.ibm.com +``` + +上例中我们的进程 ID(PID)为31094,而它的父 ID(PPID)为1(即为 init 进程 ID),并不是当前终端的进程 ID。 + +> 5.将命令+ &放在()括号中,也可以是进程不受HUP信号的影响 + +``` +[root@pvcent107 ~]## (ping www.ibm.com &) +``` + +**异常和中断的区别** + +> 中断 + +当我们在敲击键盘的同时就会产生中断,当硬盘读写完数据之后也会产生中断,所以,我们需要知道,中断是由硬件设备产生的,而它们从物理上说就是电信号,之后,它们通过中断控制器发送给CPU,接着CPU判断收到的中断来自于哪个硬件设备(这定义在内核中),最后,由CPU发送给内核,有内核处理中断。 + +下面这张图显示了中断处理的流程: + +![](https://img-blog.csdnimg.cn/6c0a43b5915e44bf8d05b6d871fd3b25.png"> + +> 异常 + +CPU处理程序的时候一旦程序不在内存中,会产生缺页异常;当运行除法程序时,当除数为0时,又会产生除0异常。 + +**异常是由CPU产生的,同时,它会发送给内核,要求内核处理这些异常** + +下面这张图显示了异常处理的流程: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/os-1555a2c9-121e-4b08-a685-2d789fc3b66b.png) + +> 相同点 + +- 最后都是由CPU发送给内核,由内核去处理 +- 处理程序的流程设计上是相似的 + +> 不同点 + +- 产生源不相同,异常是由CPU产生的,而中断是由硬件设备产生的 +- 内核需要根据是异常还是中断调用不同的处理程序 +- 中断不是时钟同步的,这意味着中断可能随时到来;异常由于是CPU产生的,所以它是时钟同步的 +- 当处理中断时,处于中断上下文中;处理异常时,处于进程上下文中 + +--- + +>作者:月伴飞鱼,转载链接:[https://mp.weixin.qq.com/s/G9ZqwEMxjrG5LbgYwM5ACQ](https://mp.weixin.qq.com/s/G9ZqwEMxjrG5LbgYwM5ACQ) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/cs/wangluo.md b/docs/cs/wangluo.md similarity index 95% rename from docs/src/cs/wangluo.md rename to docs/cs/wangluo.md index 5482d155e5..36e001d342 100644 --- a/docs/src/cs/wangluo.md +++ b/docs/cs/wangluo.md @@ -5,12 +5,12 @@ tag: - 计算机网络 --- -# 计算机网络核心知识点 +# 计算机网络核心知识点大梳理 >作者:月伴飞鱼,转载链接:[https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw](https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-608345cf-8378-4b34-bc91-ca6d2fa25da7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-608345cf-8378-4b34-bc91-ca6d2fa25da7.png) ## OSI七层模型 @@ -94,7 +94,7 @@ Linux给WIndows发包,不同系统语法不一致,如exe不能在`Linux`下 一层物理层时数据被称为**比特流**(Bits)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-d1fdc5fc-c955-4591-9c95-e297a64eccdc.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-d1fdc5fc-c955-4591-9c95-e297a64eccdc.png) ## TCP和IP模型 @@ -107,7 +107,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 - 第三层:网络层,主要是IP协议。主要负责寻址(找到目标设备的位置) - 第四层:数据链路层,主要是负责转换数字信号和物理二进制信号。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-fa4a4d20-f0db-4ba7-a31e-7772f2f132a8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-fa4a4d20-f0db-4ba7-a31e-7772f2f132a8.png) **四层网络协议的作用** @@ -127,7 +127,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 在数据链路层,对应的协议也会在IP数据包前端加上以太网的部首。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-1d3ce227-fd77-4b95-89dd-aeb786bb4b9e.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-1d3ce227-fd77-4b95-89dd-aeb786bb4b9e.png) 源设备和目标设备通过网线连接,就可以通过物理层的二进制传输数据。 @@ -135,7 +135,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 数据链路层>网络层>传输层>应用层,一层层的解码,最后就可以在浏览器中得到目标设备传送过来的**index.html**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-731f7daa-1b47-4191-828f-c6e54d650604.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-731f7daa-1b47-4191-828f-c6e54d650604.png) **TCP/IP协议族** @@ -145,7 +145,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 具体来说,在网络层是IP/ICMP协议、在传输层是TCP/UDP协议、在应用层是SMTP、FTP、以及 HTTP 等。他们都属于 TCP/IP 协议。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## 网络设备 @@ -179,7 +179,7 @@ OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实 它通过识别目的 IP 地址的**网络号**,再根据**路由表**进行数据转发 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## HTTP @@ -374,7 +374,7 @@ HTTPS 协议会对传输的数据进行加密,而加密过程是使用了非 HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过程如下: -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-c1db0431-0eee-4c80-bb60-b7508a306864.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-c1db0431-0eee-4c80-bb60-b7508a306864.png) * Client发起一个HTTPS的请求 * Server把事先配置好的公钥证书返回给客户端。 @@ -395,7 +395,7 @@ HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过 通过数字证书的方式保证服务器公钥的身份,解决冒充的风险。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-45917c49-bb08-4f88-84d2-5e8ae53acc7c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-45917c49-bb08-4f88-84d2-5e8ae53acc7c.png) ### 请求报文 @@ -403,7 +403,7 @@ HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过 HTTP 请求报文由3部分组成(请求行+请求头+请求体) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-3c00598c-43c2-44cd-96c6-ee4d40b97abd.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-3c00598c-43c2-44cd-96c6-ee4d40b97abd.png) **常见的HTTP报文头属性** @@ -431,7 +431,7 @@ HTTP 请求报文由3部分组成(请求行+请求头+请求体) 响应报文与请求报文一样,由三个部分组成(响应行,响应头,响应体) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-58884113-14dc-4cca-a63e-3320f31a4da5.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-58884113-14dc-4cca-a63e-3320f31a4da5.png) **HTTP响应报文属性** @@ -445,7 +445,7 @@ HTTP 请求报文由3部分组成(请求行+请求头+请求体) - 服务端可以设置客户端的cookie -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## TCP @@ -499,7 +499,7 @@ TCP的全部功能体现在它首部中各字段的作用 > 1. 首部前20个字符固定、后面有4n个字节是根据需而增加的选项 > 2. 故 TCP首部最小长度 = 20字节 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-8011660d-24c8-460f-ac3d-b97ad9c99b13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-8011660d-24c8-460f-ac3d-b97ad9c99b13.png) **端口**: @@ -565,7 +565,7 @@ TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲 ### 三次握手 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-f6a9438e-4eb8-4573-9ef5-30e07b8c31df.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-f6a9438e-4eb8-4573-9ef5-30e07b8c31df.png) **第一次握手**: @@ -625,7 +625,7 @@ TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲 ### 四次挥手 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-4a5e455f-5cf8-47a6-8fe4-a4c83a445f77.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-4a5e455f-5cf8-47a6-8fe4-a4c83a445f77.png) 挥手请求可以是Client端,也可以是Server端发起的,我们假设是Client端发起: @@ -688,7 +688,7 @@ TCP报文头有个字段叫Window,用于接收方通知发送方自己还有 发送方窗口内的序列号代表了那些已经被发送,但是还没有被确认的帧,或者是那些可以被发送的帧 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-d734c97b-d7d6-4af6-8674-524a81fb4dbe.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-d734c97b-d7d6-4af6-8674-524a81fb4dbe.png) 滑动窗口由四部分组成每个字节的数据都有唯一顺序的编码,随着时间发展,未确认部分与可以发送数据包编码部分向右移动,形式滑动窗口 @@ -717,7 +717,7 @@ TCP报文头有个字段叫Window,用于接收方通知发送方自己还有 主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-4bdc0051-8878-4ffc-b9db-71b73ec49cd0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-4bdc0051-8878-4ffc-b9db-71b73ec49cd0.png) **流量控制引发的死锁** @@ -796,7 +796,7 @@ TCP报文头有个字段叫Window,用于接收方通知发送方自己还有 注意:拥塞避免并非完全能够避免了阻塞,而是使网络比较不容易出现拥塞。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-314eba07-1388-4e8b-9307-8dd8d03b0dfe.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-314eba07-1388-4e8b-9307-8dd8d03b0dfe.png) **快重传算法** @@ -816,19 +816,19 @@ TCP报文头有个字段叫Window,用于接收方通知发送方自己还有 所以此时不执行慢开始算法,而是将cwnd设置为ssthresh减半后的值,然后执行拥塞避免算法,使cwnd缓慢增大。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-3a424e43-405f-494d-b700-093781b63035.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-3a424e43-405f-494d-b700-093781b63035.png) ### Socket 即套接字,是应用层 与 `TCP/IP` 协议族通信的中间软件抽象层,表现为一个封装了 TCP / IP协议族 的编程接口(API) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-eff20ef6-9d35-4075-8c53-ab52c7a46ac7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-eff20ef6-9d35-4075-8c53-ab52c7a46ac7.png) `Socket`不是一种协议,而是一个编程调用接口(`API`),属于传输层(主要解决数据如何在网络中传输) 对用户来说,只需调用Socket去组织数据,以符合指定的协议,即可通信 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## UDP @@ -874,7 +874,7 @@ TFTP、DNS、DHCP、TFTP、SNMP(简单网络管理协议)、RIP基于不可靠 UDP的报文段共有2个字段:数据字段 + 首部字段 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-a5d0d209-01db-4ee7-b63b-bf2659545702.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-a5d0d209-01db-4ee7-b63b-bf2659545702.png) **UDP报文中每个字段的含义如下:** @@ -927,7 +927,7 @@ IP地址表示为:`xxx.xxx.xxx.xxx` IP地址分A、B、C、D、E五类,其中A、B、C这三类是比较常用的IP地址,D、E类为特殊地址。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-212f3f9b-b07f-4cc9-81eb-55bb478f1b66.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-212f3f9b-b07f-4cc9-81eb-55bb478f1b66.png) ### 子网掩码 @@ -971,7 +971,7 @@ IP地址分A、B、C、D、E五类,其中A、B、C这三类是比较常用的I 但是A与C,A与D,B与C,B与D它们之间不属于同一网段,所以它们通信是要经过本地网关,然后路由器根据对方IP地址,在路由表中查找恰好有匹配到对方IP地址的直连路由,于是从另一边网关接口转发出去实现互连 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-e1d68baa-9d8c-4595-bcb5-0dc5f6e240fe.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-e1d68baa-9d8c-4595-bcb5-0dc5f6e240fe.png) **子网掩码和IP地址的关系** @@ -987,7 +987,7 @@ IP地址分A、B、C、D、E五类,其中A、B、C这三类是比较常用的I 将得出的结果转化为十进制,便得到网络地址。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-5fab32f0-20d1-4c05-bfb9-47928ceac65d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-5fab32f0-20d1-4c05-bfb9-47928ceac65d.png) ### 网关 @@ -1051,9 +1051,9 @@ DNS通过主机名,最终得到该主机名对应的IP地址的过程叫做域 将主机域名转换为ip地址,属于应用层协议,使用UDP传输。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-7d129644-dfa7-4151-ae2f-9f3f084c6be9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-7d129644-dfa7-4151-ae2f-9f3f084c6be9.png) -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-4ed70ed6-ceeb-4761-8fd4-eb2fe092273d.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/cs/wangluo-4ed70ed6-ceeb-4761-8fd4-eb2fe092273d.png) 第一步,客户端向本地DNS服务器发送解析请求 @@ -1166,7 +1166,7 @@ ARP即地址解析协议, 用于实现从 IP 地址到 MAC 地址的映射, 源主机收到ARP响应包后,将目的主机的IP和MAC地址写入ARP列表,并利用此信息发送数据,如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## 数字签名 @@ -1259,7 +1259,7 @@ SELECT * FROM user_table WHERE username=’’or 1 = 1 –- and password=’’ **例子**:甲方生成 **一对密钥** 并将其中的一把作为 **公钥** 向其它人公开,得到该公钥的 **乙方** 使用该密钥对机密信息 **进行加密** 后再发送给甲方,甲方再使用自己保存的另一把 **专用密钥** (**私钥**),对 **加密** 后的信息 **进行解密**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## 网络攻击 @@ -1390,7 +1390,7 @@ Session 的**认证过程**: 3. 有效期,Cookie 可以设置任意时间有效,而 Session 一般失效时间短 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) ## 常见面试题 @@ -1418,4 +1418,4 @@ Session 的**认证过程**: >作者:月伴飞鱼,转载链接:[https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw](https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/pdf/bat-shuati.md b/docs/download/bat-shuati.md similarity index 76% rename from docs/src/pdf/bat-shuati.md rename to docs/download/bat-shuati.md index 82d00171ce..b2f2d34764 100644 --- a/docs/src/pdf/bat-shuati.md +++ b/docs/download/bat-shuati.md @@ -1,28 +1,22 @@ --- -title: 👏下载→BAT大佬的刷题笔记 -shortTitle: 👏下载→BAT大佬的刷题笔记 +title: BAT大佬的刷题笔记 category: - - PDF + - 学习资源 tag: - PDF -description: 下载BAT大佬的刷题笔记,下载 LeetCode 刷题笔记 -head: - - - meta - - name: keywords - content: LeetCode 刷题笔记,力扣刷题笔记,leetcode Java刷题笔记,leetcode 刷题笔记github,leetcode pdf --- **大家应该都知道,现在的互联网公司面试,只要是研发岗位,基本上都跑不了算法题的折磨,所以大家在准备校招和社招的时候,或者业余时间,还是要多刷刷 LeetCode,保持状态的**。 需要刷题笔记的小伙伴请扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**100**」就可以拉取到下载链接了。 -![扫码关注后回复 100 关键字](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![扫码关注后回复「100」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) > 二哥,去年校招前准备算法时,我在 LeetCode 上刷了很多题,但总感觉题虽然刷了很多,解题能力却没怎么提高,怎么解决这种刷题效率低下的问题呢? 这是三个月前一个读者给我的私信,他的困惑让我心有戚戚焉!于是我赶紧问了身边的一些就职于大厂的朋友,他们不约而同地给我推荐了这份刷题笔记。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/download/bat-shuati-3660f789-59a4-4310-b565-8253be370904.jpg) +![](https://upload-images.jianshu.io/upload_images/1179389-1a258005b1ace2ff?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 细致地研究了一周后,我感觉发现了宝藏!赶紧发给了这位读者。前天,他回复我了,说:“二哥,你太强了,**这刷题笔记好使啊**。我按照上面提供的方法认真地刷了两个月的时间,惊奇地发现算法能力提高了,刷 LeetCode 上中等难度的题目基本不会被卡住了!” @@ -32,13 +26,13 @@ head: 不管你使用的编程语言是 Java、C++,还是 Go,都可以学习,适合刷题的同学反复学习。认真地揣摩其中的框架思维,你会发现,这是一本非常用心的刷题类书籍。笔记总共 1200 页,分编程技巧、线性表、字符串、栈队列、树、排序、查找、BFS、DFS、贪心、动态规划等。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/download/bat-shuati-8941c10e-e9de-4b50-a6e8-7b50c2a142f7.jpg) +![](https://upload-images.jianshu.io/upload_images/1179389-5903188fb91d382a?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 每个章节都会先讲解框架思维,然后挑选非常典型的十几道 LeetCode 题进行实战讲解。这份笔记不仅排版十分精美,内容也异常充实,每一题都是细致的讲解,有时候还会配上图片,就怕你搞不懂,大大的良心啊! -![](https://cdn.paicoding.com/tobebetterjavaer/images/download/bat-shuati-f2a840fd-a372-4de9-b420-4335a9e0316e.jpg) +![](https://upload-images.jianshu.io/upload_images/1179389-38d3c702ad2c6db5?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![](https://cdn.paicoding.com/tobebetterjavaer/images/download/bat-shuati-2e751738-1c8d-43b3-8e9b-d7593b62a55e.jpg) +![](https://upload-images.jianshu.io/upload_images/1179389-e69bdf5643318d02?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 以前呢,我也很讨厌刷题,觉得这就像古代的八股文一样,又臭又刻板,但互联网的公司都喜欢这么考,因为确实也找不到更好的替代方案,那如果你不准备不去刷 LeetCode 的话,面试必定挂啊! @@ -51,7 +45,7 @@ head: 二哥已经把这份刷题笔记下载好了,需要的小伙伴请扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**100**」就可以拉取到下载链接了(无套路,没加密)。 -![扫码关注后回复 100 关键字](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![扫码关注后回复「100」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) > 笔记版权归原作者(霜神)所有,出处:https://books.halfrost.com/leetcode/ diff --git a/docs/download/history.md b/docs/download/history.md new file mode 100644 index 0000000000..92e77fcc3b --- /dev/null +++ b/docs/download/history.md @@ -0,0 +1,62 @@ +# 网络日志 + +### 2022年05月21日 + +- 增加 Spring Boot 专栏 +- 增加 MySQL 文档 +- 增加微服务网关文档 + +### 2022年05月09日 + +- [增加面渣逆袭板块](/download/nixi) +- [Spring Boot 整合 MySQL-Druid](/springboot/mysql-druid) + +### 2022年04月30日 + +- 批量替换所有图片链接为阿里云的 CDN 链接 + +### 2022年04月29日 + +- [增加内部类](/oo/inner-class) + +### 2022年04月27日 + +- 修改整形到整型,By Lettuce + +### 2022年04月21日 + +- 重新定制左侧菜单 + +### 2022年04月19日 + +- [增加文档搜索功能](https://mp.weixin.qq.com/s/JVdQj-Fl9RPjt4P0y5Ws8g) + +### 2022年04月02日 + +- [杨锅锅同学提出错误:浮点多了个long, 整数少了个int](/sidebar/sanfene/javase) +- [增加数据结构与算法的学习路线](/xuexiluxian/algorithm) + +### 2022年03月31日 + +- 增加学习建议板块 + +### 2022年03月29日 + +- [修改学习路线部分的404错误](/xuexiluxian/) +- [增加Java整体学习路线](/xuexiluxian/java/yitiaolong) +- [增加Java虚拟机学习路线](/xuexiluxian/java/jvm) + +### 2022年03月27日 + +- [增加Java并发编程的内容](/home#java并发编程); +- [增加Java虚拟机模块的内容](/home#java虚拟机); + + +### 2022年03月19日 + +[Docsify 升级到 VuePress](https://mp.weixin.qq.com/s/cNtUmtVJsF0d6lQ26UFFOw) + + +### 2022年01月01日 + +[二哥的小破站终于上线了](https://mp.weixin.qq.com/s/NtOD5q95xPEs4aQpu4lGcg) \ No newline at end of file diff --git a/docs/download/java.md b/docs/download/java.md new file mode 100644 index 0000000000..ca183d8187 --- /dev/null +++ b/docs/download/java.md @@ -0,0 +1,125 @@ +--- +title: Java电子书下载 +category: + - 学习资源 +tag: + - PDF +--- + +# Java程序员常读书单📚,附下载地址 + +伟大的高尔基曾说过:“书籍是人类进步的阶梯”,读经典的书就好像是站在巨人的肩膀上,视野更开阔,思考问题的方式也会更全面。 + +讲真,挺遗憾的,大学期间,我读了不少垃圾书,比如说《21 天学会 xxx》,《3 天教你学会 xxx》。 + +直到工作后的第二年,遇到了一个非常 nice 的领导,他给我推荐了不少经典的书单,比如说《代码大全》、《编程珠玑》、《代码整洁之道》、《深入理解计算机系统》等等。 + +哇,虽然一开始读得很痛苦,但就这么坚持了一年半的时间,唉,真的发现自己的编程能力在突飞猛进呢,关键是,对业务的理解啊、对架构的设计啊、对代码的编写啊,都有了显著的提升。 + +这个书单非常的庞大,为了方便大家查找,我将它们又分门别类地上传到了 GitHub 和码云: + +- [GitHub备用地址](https://github.com/itwanger/JavaBooks) +- [Gitee备用地址](https://gitee.com/itwanger/JavaBooks) + +上传到 GitHub 和码云上还有一个好处,就是方便大家提需求,如果上面没有你想要的书籍,可以直接提 issue,我看到后就会立马去搜集和整理。 + +喜欢的话可以点个 star。 + +这是我看过的一些书: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/java-1.jpg) + +那其实很多人在学习编程的时候,很容易陷入一个误区,就是没有计划、没有路线,就导致看似投入了很多精力,但最后的学习效果却有点对不住付出的时间和精力。 + +为此,我花了将近一个月的时间,整理了这样一条学习路线,并且把我读过的电子书全部做了归类:**入门→工具→框架→数据库→并发编程→底层→性能优化→设计模式→操作系统→计算机网络→数据结构与算法→面试→架构→管理** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/java-2.jpg) + +就连颈椎康复指南都有了,这波良心吧?大家可以通过下面的方式获取,我想不管是科班还是非科班的,只要你喜欢计算机、喜欢编程,应该都会有很大的帮助。 + +需要的小伙伴请扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**pdf**」就可以拉取到下载链接了。 + +![扫码关注后回复「pdf」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) + + +## 一、编程语言 +### C语言 +- 《阮一峰老师的 C语言入门教程》 +- 《C程序设计语言》 +- 《C 和指针》 +- 《C 陷阱与缺陷》 +- 《C Primer Plus》 +### Java 语言 +- 《二哥的 Java 程序员进阶之路》,GitHub 上已经开源,持续更新 +- 《Java 编程思想》 +- 《深入浅出 Java 多线程》 +- 《深入理解 Java 虚拟机》 + +学习任何一门编程语言,一定不要浅尝辄止,因为入门都很容易,进阶却很难。如果只是蜻蜓点水,到最后可能就是竹篮打水一场空,精华的永远也学不到。 + +初学阶段,一定要多 coding,coding,coding,千万不要眼高手低。希望我的这份计算机书单能帮助到大家。 + +### C++ 语言 +- 《C++ primer》 +- 《Effective C++》 +- 《STL源码解析》 +### Python 语言 +- 《流畅的 Python》 +- 《Python编程:从入门到实践》 +- 《零基础学 Python》 +- 《用Python进行自然语言处理》 +### JavaScript 语言 +- 《JavaScript 王者归来》 +- 《你不知道的 JavaScript》 +- 《JavaScript 高级程序设计》 +## 二、数据结构与算法 +- 《算法导论》 +- 《算法 4》 +- 《编程珠玑》 +- 《编程之美》 +- 《趣学数据结构》 +## 三、计算机基础 +### 操作系统 +- 《现代操作系统》 +- 《鸟哥的 Linux 私房菜》 +### 计算机组成原理 +- 《程序是如何跑起来的》 +- 《计算机是如何跑起来的》 +- 《编码:隐匿在计算机软硬件背后的语言》 +### 计算机网络 +- 《图解 HTTP》 +- 《图解 TCP/IP》 +- 《计算机网络自顶向下》 +- 《网络是怎样连接的》 +### 数据库 +- 《SQL必知必会》 +- 《高性能 MySQL》 +- 《MySQL技术内幕 InnoDB存储引擎》 +- 《Redis 深度历险:核心原理与应用实践》 +### 四、编程实战 +- 《代码整洁之道》 +- 《阿里巴巴 Java 开发手册》 +- 《重构:改善既有代码的设计》 +- 《Effective Java》 +### 五、代码人生 +- 《黑客与画家》 +- 《人月神话》 +- 《人件》 +- 《代码大全》 +- 《数学之美》 +- 《图灵的秘密》 + +。。。。持续更新 + +当然了,我个人是有局限性的,如果大家有什么好书也可以推荐给我,我更新上来,也为后来者提供一个更体系化的书单。 + +讲真心话,随着时间的推移,我对整个计算机体系的认知也更加全面和深刻了,那这份书单真的希望能帮助到大家。 + +需要的小伙伴请扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**pdf**」就可以拉取到下载链接了。 + +![扫码关注后回复「pdf」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) + + +几年后,你将是一名善于解决实际问题的工程师,而不是一名普普通通的码农。 + +>毋庸置疑,这是一条坎坷的路,但学弟学妹们就是来披荆斩棘的,对吧? diff --git a/docs/download/jianli.md b/docs/download/jianli.md new file mode 100644 index 0000000000..138a0cc3d5 --- /dev/null +++ b/docs/download/jianli.md @@ -0,0 +1,186 @@ +--- +title: 简历下载 +category: + - 学习资源 +tag: + - PDF +--- + +大家好,我是二哥呀。 + +随着校招季的到来,不少同学都准备找工作了,也陆陆续续有同学找我看简历以及修改简历。 + +其实我以前就分享过自己的简历写法和迭代完善的过程,可能有同学没来得及看。今天客户从长沙远道而来,我要接待,所以没时间自己写了,就转一篇阿秀的文章给大家参照下。 + +需要简历模板的小伙伴请扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**简历**」就可以拉取到下载链接了。 + +![扫码关注后回复「简历」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) + +这位学弟说自己在秋招的时候没用心找工作,等毕业后才发现找的工作不满意,勉强干了半年就不想干了,然后走的社招,发现社招比校招要难一点。 + +在正式分享简历修改案例前,二哥先简单总结一下校招和社招吧。 + +**校招看基础、潜力,社招看能力、即战力**。 + +**校招**,对于某些技术问题你可以不懂不会,没关系,看的是你的潜力,你的基本功。框架不会?没事,学就是了。 + +**社招**,并不是的,社招看的是你的过往经历以及你解决问题的能力。人家招你过来是需要你来干活的,不是让你来带薪学习的,公司又不是慈善机构啊。 + +所以希望还是在校的学弟学妹们好好学学计算机基本功,打打基础。 + +程序员内功是很重要的,比如操作系统、计算机网络、数据库、计算机组成原理这些。 + +好了,废话不多说,开始今天的主题吧。 + +以下内容涉及到这位学弟的一些关键信息已脱敏,不影响大家阅读,我按照修改模块给大家分享。 + +### 专业技能 + +首先来看一下这位学弟的专业技能部分。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/jianli-2c93d164-55f7-4d2f-b0d0-f0857644a265.jpg) + +其实,这部分他写的已经挺好的了,简单大方、条理清晰。 + +也能从他的专业技能描述看出来这位学弟挺厉害的,可还是有些不足之处,我也想相应的给出了一些建议和意见。 + +**修改意见** + +1、**编程语言**部分的第一句话我建议他改一下,因为原文说的有点冗余。我建议他改成“熟悉C++编程语言,基本功扎实,熟练掌握STL模板库”。 + +后面的“阅读源码”我建议是不加的,因为STL下的思想确实比较精妙,如果别人抓住你的“阅读过部分源码”这几个字使劲问你,十有八九要吃不消,保险起见还是不要加上为好。你需要对你简历上的每一个字,或者说每一个标点符号负责。 + +2、**数据结构**部分,其实他写的挺好的了,我建议他写得更精炼具体一点,**将原文**“  熟悉常用算法(Leetcode 200道)、数据结构 (链表、栈、队列、哈希表、二叉树、B/B+树、红黑树等)  ”**改为**“熟悉常见数据结构与算法,如链表、栈、哈希表、二叉树等,以74%正确率通过力扣网站 214 道算法题”。 + +因为74%的正确率这种很精确的数字指标给面试官的直观感受更**具象**一些,并且这个正确率在力扣官网上也是可查的。 + +3、**设计模式**部分,其实他写的有点多了。对于校招而言,在设计模式上考察的并不多,没必要。 + +我建议他**改成**“了解常用设计模式如单例模式、工厂模式等”。这么写的目的也是为了方式一种情况的出现,那就是面试官很可能不问你单例、工厂,他会问你一句,你还会不会其他的设计模式,这个时候,你就可以回答“还会观察者、迭代器模式等” + +4、**数据库**部分,感觉这位小学弟有点目标过于明显了。 + +他的**原文是**“  熟悉 MySQL 关系型数据库、Redis 非关系型数据库,了解 MySQL 查询性能优化相关技术,Redis 五大数据类型,持久化机制以及主从复制技术  ”这就相当于直接让面试官问你Redis的五种数据结构是什么?持久化、主从复制又是什么了? + +面试官又不是傻子... + +我建议他改成“熟悉关系型数据库MySQL、内存型数据库Redis,了解常见查询优化技术。”这样稍微委婉一些。。。 + +下面是我当时给他的修改意见。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/jianli-f5c60946-3cdd-4ac6-9e20-8360808f8612.jpg) + +### 项目经历 + +其实一份校招简历,最重要的就是项目经历、实习经历(有则加分,没有也行)、专业技能了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/jianli-a715eaa8-86c5-41ce-aee7-cd1873e2eec8.jpg) + +可以从这位学弟的描述中看出来,他毕业后的这四个月里确实做了不少的工作,但感觉没说出来自己所做工作的意义和难点所在。 + +你可能觉得没什么,但那才是面试官在意的地方。 + +设身处地的想,如果你是一个面试官,你也希望招来的新鲜血液能力越强越好。那能力强不强在面试初期就是通过你的实习经历或者项目经验体现的啊。 + +**针对第一个项目,二哥给出了几点修改意见**: + +1、一个项目,你承担的角色是什么?主要负责人还是核心成员?包括项目的起始时间要注明。 + +2、对于其中的数据库密码验证。我建议他去掉,因为有点水平较低,还不如不写。虽说后端都是增删改查,但你不能直接说你就是搞了个密码验证啊,你这也太耿直了吧。 + +3、技术亮点部分描述不够清楚,虽然这位学弟做了很多工作,可从他的描述中是看不出来什么工作的,我给他换了一种说法。 + +**技术亮点原文描述**: + +实现与 bash 几乎完全相同的 cd 功能;使用边缘触发优化 epoll 频繁响应,使用进程池与线程池减少 CPU 运转负载;超时断开,针对连接上的客户端,若超过 30 秒未发送任何请求,断开连接,并使用环形队列技术实现高并发  ; + +其实他写的挺好的了,但我还是觉得差点意思,就是说的太白话了一些,所以我用自己的语言帮他加工润色了一下,下面是我的修改版。 + +**技术亮点修改版**: + +1、实现与 bash 几乎完全相同的 cd 功能,但反应时间更短  + +2、使用epoll边缘触发进行频繁响应,并辅助以进程池和线程池技术来减少CPU运转负载,增加系统CPU利用率  + +3、智能化断开设计:使用环形队列技术实现高并发,对于30秒内无任何网络包交互的连接进行强制断开处理,有效XXXX。 + +**是不是感觉瞬间好一点了**。。。。我只能说,**中华语言,博大精深,不服不行**。 + +以下是修改意见标注 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/jianli-5b6ff7c0-c8ad-4874-8ee8-7593e3ee1f5f.jpg) + +至此,帮他把第一个项目修改完毕了,**针对第二个项目的修改**,我就不多BB了,直接贴上来吧。 + +**以下是我给出的修改意见**: + +这个项目名改成:汽车出行APP 缺少你的角色 和 项目起始时间 该APP主要业务是什么?最终的功能有哪些?然后写上自己在其中的工作。个人工作对于该APP的加成在哪里,需要具体的量化词。 + +技术亮点中的删除相似网页,删除正确率有多少,针对该APP某某缺陷和不足,提出建立倒排索引技术,推送强相关的网页,最终提升点击率XX.XX%。 + +最后写上个人收获:学到了哪些?对XXX有了更深的认识和了解 + +**下面是当时给他做的标注**: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/jianli-f2aec848-d462-491b-9fe1-7d939ea16ed8.jpg) + +其实对于该部分的修改,我是紧紧抓住一个要点,那就是尽可能使用具化的指标即明确的信息而不是概括性的描述。 + +比如直接告诉别人提升了XX%,而不是说一句“有着较好的提升效果”;直接告诉别人“项目优化了XX秒,相较于上一版本速度加快了XX%”,而不是”经过XX技术,速度有了较大提高“。 + +这一点,我在自己的简历修改迭代文章中也提到过,很重要,特别重要,具化的指标信息给人的第一感觉就是不一样的。 + +### 工作经历、教育经历、奖项证书 + +这里比较类似,我就一起放上来说了 + +首先是原文。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/jianli-eabee6e7-0fcf-4fb7-9ca8-0ada1c604acf.jpg) + + +我给的修改意见批注。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/jianli-bb7279ad-dd46-47ea-8f13-37d5caaf404e.jpg) + +1、在工作经历这里,注意细节问题,比如你的岗位名称是什么?你在实习过程中,你的工作有哪些?在这过程中用到了哪些技能? + +不要直接就是一句话写出来,不明不白的,没什么愿意看的。人都是视觉动物,都希望看到整齐有序的内容。 + +这里的整齐有序不仅要求你的排版整齐,你想要表达的意思也要有序。 + +比如把你的工作或者实习分为团队工作、个人工作、个人收获,挨个展开。 + +2、由于这位学弟已经毕业了,已经不能再以校招身份去面试找工作了,所以教育经历这里其实已经没必要写在校学习的课程了。 + +一旦你踏入社会,失去了应届生身份你就不再是校招生了,这个时候看的就是你的能力了。 + +3、对于一些细节问题记得做好,关键字该加粗加粗。比如有分量的国奖还是值得一个加粗显示的。 + +4、如果你过了六级,那直接写**六级**,分数还可以的话就写上分数;分数比较低的话,比如430这种刚飘过,就写六级就行。 + +如果六级没过,那就写上四级吧。 + +### 简历模板下载方式 + +我把这位小学弟和我自己以前的简历模板放在一起了,需要简历模板的小伙伴请扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**简历**」就可以拉取到下载链接了。 + +![扫码关注后回复「简历」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) + +记住!简历并不是单纯意义上的自我介绍,比如说我是沉默王二,今年 18 岁,来自洛阳,毕业于某某学校。这样的简历太苍白了。 + +简历就好像电梯广告的单页一样,它富有鲜活的生命力,它在呐喊,它不需要过多的润色,只需要铿锵有力、赤裸裸的“炫耀”。 + +比如说我是沉默王二, 2019 年参与了 XXX 项目的开发。作为项目团队的核心 开发人员,我不仅能够提前完成自己的开发任务,还设计了一个高效的缓存中间件,大大提高了系统的性能。 + +该中间件上线后,Web 前端性能从 10 QPS 提升到 120 QPS,服务器由 10 台缩减为 5 台。 + +鉴于之前的良好表现,我在 2020 年升任项目的主要负责人,虽然小组成员只有 15 个,但硬生生地肩负起了每天超过 2000 万的 PV。 + +看,这样的简历是不是让人耳目一新,证明自己价值的同时,没有过多的粉饰,让招聘方觉得你很靠谱,迫切地想要把你这个人才“抢”到手,免费被别的公司挖走了。 + +简历上的内容不要太多,尽量不要超过一页,因为招聘方没有那么多时间去翻看你的简历。我是挺相信第一印象的,好的简历看一眼就会过目不忘,真的。 + +我是二哥呀,人生最可怕的事莫过于在别人放弃你之前,你先放弃了自己,我们下期再见。 + +>作者:阿秀,转载链接:[https://mp.weixin.qq.com/s/soVldFzBbqwm_vM35afFvg](https://mp.weixin.qq.com/s/soVldFzBbqwm_vM35afFvg) diff --git a/docs/src/download/learn-jianyi.md b/docs/download/learn-jianyi.md similarity index 100% rename from docs/src/download/learn-jianyi.md rename to docs/download/learn-jianyi.md diff --git a/docs/src/download/nicearticle.md b/docs/download/nicearticle.md similarity index 98% rename from docs/src/download/nicearticle.md rename to docs/download/nicearticle.md index cdf95c925b..0c3ae181b8 100644 --- a/docs/src/download/nicearticle.md +++ b/docs/download/nicearticle.md @@ -5,7 +5,7 @@ ### 图文教程 - [Java 正则表达式详解](https://segmentfault.com/a/1190000009162306) -- [Java 正则从入门到精通](https://dunwu.github.io/javacore/pages/4c1dd4/) +- [Java 正则从入门到精通](https://dunwu.github.io/javacore/advanced/java-regex.html) - [GitHub 上优质的 Java 知识总结项](https://mp.weixin.qq.com/s/-lQ2PTEO4F2d92GDDxKVpw) - [处于萌芽阶段的 Java 核心知识库](https://mp.weixin.qq.com/s/_Q7lopxM1sJtMA-NOE_v3A) - [大数据入门指南 ](https://github.com/heibaiying/BigData-Notes) @@ -91,6 +91,8 @@ - [降薪 45%,从互联网回到国企](https://mp.weixin.qq.com/s/qHGdIuA32X-zydbMTKDPuA) - [学弟在微软的这六个月](https://mp.weixin.qq.com/s/08Ax1ArAjchemjUXih7zNw) - [找个程序员做老公/男票有多爽???](https://mp.weixin.qq.com/s/mK0yaen1mhCoWZ6ZLC5vQg) +- [研究所月入两万,是什么体验?](/manongshenghuo/yanjiusuo-20wan.md) +- [裸辞全职做外包一个月的感受](/manongshenghuo/waibao-1geyue.md) - [转眼就来字节六个月了,真的不一样](https://mp.weixin.qq.com/s/jG7DLrCFf_pYoMLFiVbaaA) - [在监狱里编程是一种什么体验(上)?](https://mp.weixin.qq.com/s/ci5Meem_d3g2BphDmMP8VQ) - [29 岁,非科班零基础,想兼职做外包。。](https://mp.weixin.qq.com/s/CTTlnXNXY9j3Bm3_4gmIbw) @@ -204,8 +206,10 @@ > [!TIP] > 开发过程中遇到的一些典型问题,该如何解决? +- [Log4j2突发重大漏洞](/shigu/log4j2.md) - [重现了一波 Log4j2 核弹级漏洞,同事的电脑沦为炮灰](https://mp.weixin.qq.com/s/zXzJVxRxMUnoyJs6_NojMQ) - [生成订单30分钟未支付,则自动取消,该怎么实现?](https://mp.weixin.qq.com/s/J6jb_Dt3C49CIjYBTrN4gQ) +- [两天两夜,1M图片优化到100kb!](/shigu/image-yasuo.md) - [内部群炸了锅,隔壁同事真删库了啊。。](https://mp.weixin.qq.com/s/lvyoN1gHCWhcPqudcjcRgQ) - [B 站崩了](https://mp.weixin.qq.com/s/PfJe5rXednkXTq8EKT0xpw) - [因为一个低级错误,生产数据库崩溃了将近半个小时](https://mp.weixin.qq.com/s/ec6u8WsPt7zJ0eul8sPEhg) @@ -223,6 +227,7 @@ ## MySQL重要知识点 +- [从京东到家程序员删库被判 10 个月来聊聊 MySQL 数据备份的杀手锏](/mysql/deletedb-binlog-weiguanjishu.md) - [深入浅出MySQL crash safe](https://tech.youzan.com/shen-ru-qian-chu-mysql-crash-safe/) ## 待收录文章 diff --git a/docs/download/pdf.md b/docs/download/pdf.md new file mode 100644 index 0000000000..6b1e7e4d99 --- /dev/null +++ b/docs/download/pdf.md @@ -0,0 +1,24 @@ +--- +title: PDF干货笔记下载 +category: + - 学习资源 +tag: + - PDF +--- + +# PDF干货笔记📚,附下载地址 + + +>- **找不到优质的学习资源**?这些问题在这里统统都可以找到答案。 +>- 我会把自己十多年的学习资源毫不保留的分享出来。 + +- [👏下载→最全最硬核的Java面试 “备战” 资料](https://mp.weixin.qq.com/s/US5nTxbC2nYc1hWpn5Bozw) +- [👏下载→深入浅出Java多线程](https://mp.weixin.qq.com/s/pxKrjw_5NTdZfHOKCkwn8w) +- [👏下载→GitHub星标115k+的Java教程](https://mp.weixin.qq.com/s/d7Z0QoChNuP9bTwAGh2QCw) +- [👏下载→重学Java设计模式](https://mp.weixin.qq.com/s/PH5AizUAnTz0CuvJclpAKw) +- [👏下载→Java版LeetCode刷题笔记](https://mp.weixin.qq.com/s/FyoOPIMGcaeH0z5RMhxtaQ) +- [👏下载→阮一峰C语言入门教程](/download/yuanyifeng-c-language.md) +- [👏下载→BAT大佬的刷题笔记](/download/bat-shuati.md) +- [👏下载→给操作系统捋条线](https://mp.weixin.qq.com/s/puTGbgU7xQnRcvz5hxGBHA) +- [👏下载→豆瓣9.1分,Pro Git中文版](/download/progit.md) +- [👏下载→简历模板](/download/jianli.md) \ No newline at end of file diff --git a/docs/download/progit.md b/docs/download/progit.md new file mode 100644 index 0000000000..781d7473cb --- /dev/null +++ b/docs/download/progit.md @@ -0,0 +1,48 @@ +--- +title: 豆瓣9.1分,Pro Git中文版 +category: + - 学习资源 +tag: + - PDF +--- + +今天给大家分享一本个人最近看过觉得非常不错的Git开源手册,可能有些小伙伴也看过了,我是最近在通勤路上用PAD看的。这本开源手册,它除了有**PDF版**,还有**epub电子书版**,非常适合电子阅读: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/progit-41240dae-f097-4986-b1a3-20f6a8035732.jpg) + +需要的小伙伴请扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**git**」就可以拉取到下载链接了。 + +![扫码关注后回复「git」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) + +相信看完对于个人Git知识体系的梳理和掌握是非常有帮助的。 + +这本手册在豆瓣上评价极高,之前9.3,现在也有9.1的高分,其作者是GitHub的员工,内容主要侧重于各种场合中的惯用法和底层原理的讲述,手册中还针对不同的使用场景,设计了几个合适的版本管理策略。简而言之,这本手册无论是对于初学者还是想进一步了解Git工作原理的开发者都非常合适。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/progit-66757d24-4bcb-4084-9e7f-8e9e32d517c4.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/progit-b829ef9a-3c8f-4e91-b88b-3eeb1692d8d9.jpg) + +这个手册一共分为十章,详细内容如下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/progit-33b772d5-fa59-4181-8831-9efe9c1b11ea.jpg) + +**手册中部分内容展示如下:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/progit-f8c62f18-ceaa-4de1-b7ad-961cd1418bfb.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/progit-d82932c6-5a30-4ed6-86e5-e4ca468e8a13.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/progit-ca7c7781-7c3f-4729-9cd7-2c050b660e4e.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/progit-91c726c1-a10d-41fb-8b2a-a5228d741106.jpg) + +**需要该Git手册PDF+epub电子书的小伙伴:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/download/progit-eff913ad-4635-498f-8a04-01a03380e84a.jpg) + +可扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**git**」就可以拉取到下载链接了。 + +![扫码关注后回复「git」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) + + +好了,这次资源分享就到这里!后续如果遇到有用的工具或者资源,依然还会持续分享,也欢迎大家多多安利和交流,一起分享成长。 diff --git a/docs/src/pdf/yuanyifeng-c-language.md b/docs/download/yuanyifeng-c-language.md similarity index 84% rename from docs/src/pdf/yuanyifeng-c-language.md rename to docs/download/yuanyifeng-c-language.md index fccde50d59..b0f653a407 100644 --- a/docs/src/pdf/yuanyifeng-c-language.md +++ b/docs/download/yuanyifeng-c-language.md @@ -1,26 +1,20 @@ --- -title: 👏下载→阮一峰C语言入门教程 -shortTitle: 👏下载→阮一峰C语言入门教程 +title: 阮一峰-C语言入门教程 category: - - PDF + - 学习资源 tag: - PDF -description: 给操作系统学习资料下载 -head: - - - meta - - name: keywords - content: C语言教程,阮一峰,阮一峰 C语言,C语言入门教程 --- 给大家报告下,阮一峰老师的《**C语言入门教程**》于 2021 年 9 月 7 日上线了! 对,和往常一样,这个教程是开源的,采用知识共享许可证,源码托管在 GitHub,大家可以自由使用。 ->[https://github.com/wangdoc/clang-tutorial](https://github.com/wangdoc/clang-tutorial) +>https://github.com/wangdoc/clang-tutorial 在线阅读地址也有: ->[https://wangdoc.com/clang/](https://wangdoc.com/clang/) +>https://wangdoc.com/clang/ 我第一时间就拜读了一遍,受益匪浅!可以说目前我见到的最好的 C语言入门教程了,没有之一! @@ -35,7 +29,7 @@ head: 需要的小伙伴请扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**08**」就可以拉取到下载链接了。 -![扫码关注后回复「08」关键字](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![扫码关注后回复「08」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) 也可以微信搜「**沉默王二**」关注后回复关键字「**08**」。 @@ -45,7 +39,7 @@ head: 那配上阮一峰老师的这个在线文档教程,可以说是完美! -![](https://cdn.paicoding.com/tobebetterjavaer/images/download/yuanyifeng-c-language-4a28a9bd-0db4-419c-8aaf-fa102323282d.png) +![](https://files.mdnice.com/user/3903/96b19f0e-4721-4b86-b69b-3eeb78ac3410.png) 我对这份教程是非常满意的,该讲的地方都讲到了,示例也给了很多,对初学者来说,完全够用了。 @@ -77,9 +71,9 @@ C语言在武林界的地位就相当于少林的地位,天下武功皆出少 不过网上也有蛮多在线编译器的,可以直接在网页上模拟运行 C 代码,查看结果,非常方便。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/download/yuanyifeng-c-language-4dfd8f91-893f-401f-81ad-488e08934c07.png) +![](https://files.mdnice.com/user/3903/e565681d-ee91-4be3-bb1a-bee34f0e87a6.png) ->CodingGround网址:[https://www.tutorialspoint.com/compile_c_online.php](https://www.tutorialspoint.com/compile_c_online.php) +>CodingGround网址:https://www.tutorialspoint.com/compile_c_online.php C 语言是一种编译型语言,源码是文本文件,本身是无法执行的,需要通过编译器,生成二进制的可执行文件。 @@ -99,7 +93,7 @@ C语言中,指针是令初学者头痛的一块内容,所以我这里简单 >需要阮一峰老师的这份《C语言入门教程》的小伙伴请扫描下方的二维码关注作者的原创公众号「**沉默王二**」回复关键字「**08**」就可以拉取到下载链接了。 -![扫码关注后回复「08」关键字](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![扫码关注后回复「08」关键字](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) 也可以微信搜「**沉默王二**」关注后回复关键字「**08**」。 diff --git a/docs/elasticsearch/rumen.md b/docs/elasticsearch/rumen.md new file mode 100644 index 0000000000..8b5eaaef7e --- /dev/null +++ b/docs/elasticsearch/rumen.md @@ -0,0 +1,189 @@ +--- +category: + - Java企业级开发 +tag: + - Elasticsearch +--- + +# 全文搜索引擎Elasticsearch入门教程 + +学习真的是一件令人开心的事情,上次分享了 [Redis 入门](https://mp.weixin.qq.com/s/NPJkMy5RppyFk9QhzHxhrw)的文章后,收到了很多小伙伴的鼓励,比如说:“哎呀,不错呀,二哥,通俗易懂,十分钟真的入门了”。瞅瞅,瞅瞅,我决定再接再厉,入门一下 Elasticsearch,因为我们公司的商城系统升级了,需要用 Elasticsearch 做商品的搜索。 + +不过,我首先要声明一点,我对 Elasticsearch 并没有进行很深入的研究,仅仅是因为要用,就学一下。但作为一名负责任的技术博主,我是用心的,为此还特意在某某时间上买了一门视频课程,作者叫阮一鸣。说实话,他光秃秃的头顶让我对这门课程产生了浓厚的兴趣。 + +经过三天三夜的学习,总算是入了 Elasticsearch 的门,我就决定把这些心得体会分享出来,感兴趣的小伙伴可以作为参考。遇到文章中有错误的地方,不要手下留情,过来捶我,只要不打脸就好。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-ebb2bdbc-2cdb-4540-b48f-41f92c848f2f.jpg) + + +### 01、Elasticsearch 是什么 + +>Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。 + +以上引用来自于官方,不得不说,解释得蛮文艺的。意料之中和意料之外,这两个词让我想起来了某一年的高考作文题(情理之中和意料之外)。 + +Elastic Stack 又是什么呢?整个架构图如下图(来源于网络,侵删)所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-04b04318-25c9-4eb5-895e-9c608a4b26f9.jpg) + +信息量比较多,对吧?那就记住一句话吧,Elasticsearch 是 Elastic Stack 的核心。 + +国内外的很多知名公司都在用 Elasticsearch,比如说滴滴、今日头条、谷歌、微软等等。Elasticsearch 有很多强大的功能,比如说全文搜索、购物推荐、附近定位推荐等等。 + +理论方面的内容就不说太多了,我怕小伙伴们会感到枯燥。毕竟入门嘛,实战才重要。 + +### 02、安装 Elasticsearch + +Elasticsearch 是由 Java 开发的,所以早期的版本需要先在电脑上安装 JDK 进行支持。后来的版本中内置了 Java 环境,所以直接下载就行了。Elasticsearch 针对不同的操作系统有不同的安装包,我们这篇入门的文章就以 Windows 为例吧。 + +下载地址如下: + +[https://www.elastic.co/cn/downloads/elasticsearch](https://www.elastic.co/cn/downloads/elasticsearch) + +最新的版本是 7.6.2,280M 左右。但我硬生生花了 10 分钟的时间才下载完毕,不知道是不是连通的 200M 带宽不给力,还是官网本身下载的速度就慢,反正我去洗了 6 颗葡萄吃完后还没下载完。 + +Elasticsearch 是免安装的,只需要把 zip 包解压就可以了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-07da0521-74eb-4a90-b17f-59258e622609.jpg) + +1)bin 目录下是一些脚本文件,包括 Elasticsearch 的启动执行文件。 + +2)config 目录下是一些配置文件。 + +3)jdk 目录下是内置的 Java 运行环境。 + +4)lib 目录下是一些 Java 类库文件。 + +5)logs 目录下会生成一些日志文件。 + +6)modules 目录下是一些 Elasticsearch 的模块。 + +7)plugins 目录下可以放一些 Elasticsearch 的插件。 + +直接双击 bin 目录下的 elasticsearch.bat 文件就可以启动 Elasticsearch 服务了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-7dd19afd-1aeb-49b6-a07c-f11e139fe3d3.jpg) + +输出的日志信息有点多,不用细看,注意看到有“started”的字样就表明启动成功了。为了进一步确认 Elasticsearch 有没有启动成功,可以在浏览器的地址栏里输入 `http://localhost:9200` 进行查看(9200 是 Elasticsearch 的默认端口号)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-51f269c2-7482-494a-8a04-6585f20176a7.jpg) + +你看,为了 Search。 + +那如何停止服务呢?可以直接按下 `Ctrl+C` 组合键——粗暴、壁咚。 + +### 03、安装 Kibana + +通过 Kibana,我们可以对 Elasticsearch 服务进行可视化操作,就像在 Linux 操作系统下安装一个图形化界面一样。 + +下载地址如下: + +[https://www.elastic.co/cn/downloads/kibana](https://www.elastic.co/cn/downloads/kibana) + +最新的版本是 7.6.2,284M 左右,体积和 Elasticsearch 差不多。选择下载 Windows 版,zip 格式的,完成后直接解压就行了。下载的过程中又去洗了 6 颗葡萄吃,狗头。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-12372ee6-acc0-4425-964b-ca32886f17ce.jpg) + +包目录不再一一解释了,进入 bin 目录下,双击运行 kibana.bat 文件,启动 Kibana 服务。整个过程比 Elasticsearch 要慢一些,当看到 `[Kibana][http] http server running` 的信息后,说明服务启动成功了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-784d70ef-b6e7-4312-85f1-36ace9b2a5bd.jpg) + +在浏览器地址栏输入 `http://localhost:5601` 查看 Kibana 的图形化界面。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-e6f64545-a925-4bb4-a25e-44129832fb4e.jpg) + +由于当前的 Elasticsearch 服务端中还没有任何数据,所以我们可以选择「Try Our Sample Data」导入 Kibana 提供的模拟数据体验一下。下图是导入电商数据库的看板页面,是不是很丰富? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-a16d99ff-272d-43bb-aa94-23b240cc464b.jpg) + +打开 Dev Tools 面板,可以看到一个简单的 DSL 查询语句(一种完全基于 JSON 的特定于领域的语言),点击「运行」按钮后就可以看到 JSON 格式的数据了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-5c44bd79-d3a9-49fb-9414-04dc38840cfb.jpg) + +### 04、Elasticsearch 的关键概念 + +在进行下一步之前,需要先来理解 Elasticsearch 中的几个关键概念,比如说什么是索引,什么是类型,什么是文档等等。Elasticsearch 既然是一个数据引擎,它里面的一些概念就和 MySQL 有一定的关系。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-ad2b2f8c-5a19-4c5e-9bc7-cf7ba17830bf.jpg) + +看完上面这幅图(来源于网络,侵删),是不是瞬间就清晰了。向 Elasticsearch 中存储数据,其实就是向 Elasticsearch 中的 index 下面的 type 中存储 JSON 类型的数据。 + + +### 05、在 Java 中使用 Elasticsearch + +有些小伙伴可能会问,“二哥,我是一名 Java 程序员,我该如何在 Java 中使用 Elasticsearch 呢?”这个问题问得好,这就来,这就来。 + +Elasticsearch 既然内置了 Java 运行环境,自然就提供了一系列 API 供我们操作。 + +第一步,在项目中添加 Elasticsearch 客户端依赖: + +``` + + org.elasticsearch.client + elasticsearch-rest-high-level-client + 7.6.2 + +``` + +第二步,新建测试类 ElasticsearchTest: + +```java +public class ElasticsearchTest { + public static void main(String[] args) throws IOException { + + RestHighLevelClient client = new RestHighLevelClient( + RestClient.builder( + new HttpHost("localhost", 9200, "http"))); + + IndexRequest indexRequest = new IndexRequest("writer") + .id("1") + .source("name", "沉默王二", + "age", 18, + "memo", "一枚有趣的程序员"); + IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT); + + GetRequest getRequest = new GetRequest("writer", "1"); + + GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT); + String sourceAsString = getResponse.getSourceAsString(); + + System.out.println(sourceAsString); + client.close(); + } +} +``` + +1)RestHighLevelClient 为 Elasticsearch 提供的 REST 客户端,可以通过 HTTP 的形式连接到 Elasticsearch 服务器,参数为主机名和端口号。 + +有了 RestHighLevelClient 客户端,我们就可以向 Elasticsearch 服务器端发送请求并获取响应。 + +2)IndexRequest 用于向 Elasticsearch 服务器端添加一个索引,参数为索引关键字,比如说“writer”,还可以指定 id。通过 source 的方式可以向当前索引中添加文档数据源(键值对的形式)。 + +有了 IndexRequest 对象后,可以调用客户端的 `index()` 方法向 Elasticsearch 服务器添加索引。 + +3)GetRequest 用于向 Elasticsearch 服务器端发送一个 get 请求,参数为索引关键字,以及 id。 + +有了 GetRequest 对象后,可以调用客户端的 `get()` 方法向 Elasticsearch 服务器获取索引。`getSourceAsString()` 用于从响应中获取文档数据源(JSON 字符串的形式)。 + +好了,来看一下程序的输出结果: + +``` +{"name":"沉默王二","age":18,"memo":"一枚有趣的程序员"} +``` + +完全符合我们的预期,perfect! + +也可以通过 Kibana 的 Dev Tools 面板查看“writer”索引,结果如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/elasticsearch/rumen-64baa243-0075-436e-a070-f28813fee284.jpg) + + + + +### 06、鸣谢 + +好了,我亲爱的小伙伴们,以上就是本文的全部内容了,是不是看完后很想实操一把 Elasticsearch,赶快行动吧!如果你在学习的过程中遇到了问题,欢迎随时和我交流,虽然我也是个菜鸟,但我有热情啊。 + +另外,如果你想写入门级别的文章,这篇就是最好的范例。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/exception/gailan.md b/docs/exception/gailan.md new file mode 100644 index 0000000000..6143fd980f --- /dev/null +++ b/docs/exception/gailan.md @@ -0,0 +1,464 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 一文读懂Java异常处理 + +## 一、什么是异常 + +“二哥,今天就要学习异常了吗?”三妹问。 + +“是的。只有正确地处理好异常,才能保证程序的可靠性,所以异常的学习还是很有必要的。”我说。 + +“那到底什么是异常呢?”三妹问。 + +“异常是指中断程序正常执行的一个不确定的事件。当异常发生时,程序的正常执行流程就会被打断。一般情况下,程序都会有很多条语句,如果没有异常处理机制,前面的语句一旦出现了异常,后面的语句就没办法继续执行了。” + +“有了异常处理机制后,程序在发生异常的时候就不会中断,我们可以对异常进行捕获,然后改变程序执行的流程。” + +“除此之外,异常处理机制可以保证我们向用户提供友好的提示信息,而不是程序原生的异常信息——用户根本理解不了。” + +“不过,站在开发者的角度,我们更希望看到原生的异常信息,因为这有助于我们更快地找到 bug 的根源,反而被过度包装的异常信息会干扰我们的视线。” + +“Java 语言在一开始就提供了相对完善的异常处理机制,这种机制大大降低了编写可靠程序的门槛,这也是 Java 之所以能够流行的原因之一。” + +“那导致程序抛出异常的原因有哪些呢?”三妹问。 + +比如说: + +- 程序在试图打开一个不存在的文件; +- 程序遇到了网络连接问题; +- 用户输入了糟糕的数据; +- 程序在处理算术问题时没有考虑除数为 0 的情况; + +等等等等。 + +挑个最简单的原因来说吧。 + +```java +public class Demo { + public static void main(String[] args) { + System.out.println(10/0); + } +} +``` + +这段代码在运行的时候抛出的异常信息如下所示: + +``` +Exception in thread "main" java.lang.ArithmeticException: / by zero + at com.itwanger.s41.Demo.main(Demo.java:8) +``` + +“你看,三妹,这个原生的异常信息对用户来说,显然是不太容易理解的,但对于我们开发者来说,简直不要太直白了——很容易就能定位到异常发生的根源。” + +## 二、Exception和Error的区别 + +“哦,我知道了。下一个问题,我经常看到一些文章里提到 Exception 和 Error,二哥你能帮我解释一下它们之间的区别吗?”三妹问。 + +“这是一个好问题呀,三妹!” + +从单词的释义上来看,error 为错误,exception 为异常,错误的等级明显比异常要高一些。 + +从程序的角度来看,也的确如此。 + +Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉,比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存,导致出现的错误,这种错误的出现,对于程序来说是致命的。 + +Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救。 + +比如说之前提到的 ArithmeticException,很明显是因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作,当然了,更好的做法是直接对除数进行判断,如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。 + +## 三、checked和unchecked异常 + +“三妹,还能想到其他的问题吗?” + +“嗯,不用想,二哥,我已经提前做好预习工作了。”三妹自信地说,“异常又可以分为 checked 和 unchecked,它们之间又有什么区别呢?” + +“哇,三妹,果然又是一个好问题呢。” + +checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;而 unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出。 + +“我先画一幅思维导图给你感受一下。” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/exception/gailan-01.png) + +首先,Exception 和 Error 都继承了 Throwable 类。换句话说,只有 Throwable 类(或者子类)的对象才能使用 throw 关键字抛出,或者作为 catch 的参数类型。 + +面试中经常问到的一个问题是,NoClassDefFoundError 和 ClassNotFoundException 有什么区别? + +“三妹你知道吗?” + +“不知道,二哥,你解释下呗。” + +它们都是由于系统运行时找不到要加载的类导致的,但是触发的原因不一样。 + +- NoClassDefFoundError:程序在编译时可以找到所依赖的类,但是在运行时找不到指定的类文件,导致抛出该错误;原因可能是 jar 包缺失或者调用了初始化失败的类。 +- ClassNotFoundException:当动态加载 Class 对象的时候找不到对应的类时抛出该异常;原因可能是要加载的类不存在或者类名写错了。 + + +其次,像 IOException、ClassNotFoundException、SQLException 都属于 checked 异常;像 RuntimeException 以及子类 ArithmeticException、ClassCastException、ArrayIndexOutOfBoundsException、NullPointerException,都属于 unchecked 异常。 + +unchecked 异常可以不在程序中显示处理,就像之前提到的 ArithmeticException 就是的;但 checked 异常必须显式处理。 + +比如说下面这行代码: + +```java +Class clz = Class.forName("com.itwanger.s41.Demo1"); +``` + +如果没做处理,比如说在 Intellij IDEA 环境下,就会提示你这行代码可能会抛出 `java.lang.ClassNotFoundException`。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/exception/gailan-02.png) + +建议你要么使用 try-catch 进行捕获: + +```java +try { + Class clz = Class.forName("com.itwanger.s41.Demo1"); +} catch (ClassNotFoundException e) { + e.printStackTrace(); +} +``` + +注意打印异常堆栈信息的 `printStackTrace()` 方法,该方法会将异常的堆栈信息打印到标准的控制台下,如果是测试环境,这样的写法还 OK,如果是生产环境,这样的写法是不可取的,必须使用日志框架把异常的堆栈信息输出到日志系统中,否则可能没办法跟踪。 + +要么在方法签名上使用 throws 关键字抛出: + +```java +public class Demo1 { + public static void main(String[] args) throws ClassNotFoundException { + Class clz = Class.forName("com.itwanger.s41.Demo1"); + } +} +``` + +这样做的好处是不需要对异常进行捕获处理,只需要交给 Java 虚拟机来处理即可;坏处就是没法针对这种情况做相应的处理。 + +“二哥,针对 checked 异常,我在知乎上看到一个帖子,说 Java 中的 checked 很没有必要,这种异常在编译期要么 try-catch,要么 throws,但又不一定会出现异常,你觉得这样的设计有意义吗?”三妹提出了一个很尖锐的问题。 + +“哇,这种问题问的好。”我不由得对三妹心生敬佩。 + +“的确,checked 异常在业界是有争论的,它假设我们捕获了异常,并且针对这种情况作了相应的处理,但有些时候,根本就没法处理。”我说,“就拿上面提到的 ClassNotFoundException 异常来说,我们假设对其进行了 try-catch,可真的出现了 ClassNotFoundException 异常后,我们也没多少的可操作性,再 `Class.forName()` 一次?” + +另外,checked 异常也不兼容函数式编程,后面如果你写 Lambda/Stream 代码的时候,就会体验到这种苦涩。 + +当然了,checked 异常并不是一无是处,尤其是在遇到 IO 或者网络异常的时候,比如说进行 Socket 链接,我大致写了一段: + +```java +public class Demo2 { + private String mHost; + private int mPort; + private Socket mSocket; + private final Object mLock = new Object(); + + public void run() { + } + + private void initSocket() { + while (true) { + try { + Socket socket = new Socket(mHost, mPort); + synchronized (mLock) { + mSocket = socket; + } + break; + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} +``` + +当发生 IOException 的时候,socket 就重新尝试连接,否则就 break 跳出循环。意味着如果 IOException 不是 checked 异常,这种写法就略显突兀,因为 IOException 没办法像 ArithmeticException 那样用一个 if 语句判断除数是否为 0 去规避。 + +或者说,强制性的 checked 异常可以让我们在编程的时候去思考,遇到这种异常的时候该怎么更优雅的去处理。显然,Socket 编程中,肯定是会遇到 IOException 的,假如 IOException 是非检查型异常,就意味着开发者也可以不考虑,直接跳过,交给 Java 虚拟机来处理,但我觉得这样做肯定更不合适。 + +## 四、关于 try-catch-finally + +“二哥,你能告诉我 throw 和 throws 两个关键字的区别吗?”三妹问。 + +“throw 关键字,用于主动地抛出异常;正常情况下,当除数为 0 的时候,程序会主动抛出 ArithmeticException;但如果我们想要除数为 1 的时候也抛出 ArithmeticException,就可以使用 throw 关键字主动地抛出异常。”我说。 + +```java +throw new exception_class("error message"); +``` + +语法也非常简单,throw 关键字后跟上 new 关键字,以及异常的类型还有参数即可。 + +举个例子。 + +```java +public class ThrowDemo { + static void checkEligibilty(int stuage){ + if(stuage<18) { + throw new ArithmeticException("年纪未满 18 岁,禁止观影"); + } else { + System.out.println("请认真观影!!"); + } + } + + public static void main(String args[]){ + checkEligibilty(10); + System.out.println("愉快地周末.."); + } +} +``` + +这段代码在运行的时候就会抛出以下错误: + +``` +Exception in thread "main" java.lang.ArithmeticException: 年纪未满 18 岁,禁止观影 + at com.itwanger.s43.ThrowDemo.checkEligibilty(ThrowDemo.java:9) + at com.itwanger.s43.ThrowDemo.main(ThrowDemo.java:16) +``` + +“throws 关键字的作用就和 throw 完全不同。”我说,“[异常处理机制](https://mp.weixin.qq.com/s/fXRJ1xdz_jNSSVTv7ZrYGQ)这小节中讲了 checked exception 和 unchecked exception,也就是检查型异常和非检查型异常;对于检查型异常来说,如果你没有做处理,编译器就会提示你。” + +`Class.forName()` 方法在执行的时候可能会遇到 `java.lang.ClassNotFoundException` 异常,一个检查型异常,如果没有做处理,IDEA 就会提示你,要么在方法签名上声明,要么放在 try-catch 中。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/exception/throw-throws-01.png) + +“那什么情况下使用 throws 而不是 try-catch 呢?”三妹问。 + +“假设现在有这么一个方法 `myMethod()`,可能会出现 ArithmeticException 异常,也可能会出现 NullPointerException。这种情况下,可以使用 try-catch 来处理。”我回答。 + +```java +public void myMethod() { + try { + // 可能抛出异常 + } catch (ArithmeticException e) { + // 算术异常 + } catch (NullPointerException e) { + // 空指针异常 + } +} +``` + +“但假设有好几个类似 `myMethod()` 的方法,如果为每个方法都加上 try-catch,就会显得非常繁琐。代码就会变得又臭又长,可读性就差了。”我继续说。 + +“一个解决办法就是,使用 throws 关键字,在方法签名上声明可能会抛出的异常,然后在调用该方法的地方使用 try-catch 进行处理。” + +```java +public static void main(String args[]){ + try { + myMethod1(); + } catch (ArithmeticException e) { + // 算术异常 + } catch (NullPointerException e) { + // 空指针异常 + } +} +public static void myMethod1() throws ArithmeticException, NullPointerException{ + // 方法签名上声明异常 +} +``` + +“好了,我来总结下 throw 和 throws 的区别,三妹,你记一下。” + + 1)throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式的抛出异常。 + +2)throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象。 + +示例。 + +``` +throws ArithmeticException; +``` + +``` +throw new ArithmeticException("算术异常"); +``` + + 3)throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。 + +4)throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。 + +## 五、关于 throw 和 throws + +“二哥,[上一节](https://mp.weixin.qq.com/s/fXRJ1xdz_jNSSVTv7ZrYGQ)你讲了异常处理机制,这一节讲什么呢?”三妹问。 + +“该讲 try-catch-finally 了。”我说,“try 关键字后面会跟一个大括号 `{}`,我们把一些可能发生异常的代码放到大括号里;`try` 块后面一般会跟 `catch` 块,用来处理发生异常的情况;当然了,异常不一定会发生,为了保证发不发生异常都能执行一些代码,就会跟一个 `finally` 块。” + +“具体该怎么用呀,二哥?”三妹问。 + +“别担心,三妹,我一一来说明下。”我说。 + +`try` 块的语法很简单: + +```java +try{ +// 可能发生异常的代码 +} +``` + +“注意啊,三妹,如果一些代码确定不会抛出异常,就尽量不要把它包裹在 `try` 块里,因为加了异常处理的代码执行起来要比没有加的花费更多的时间。” + +`catch` 块的语法也很简单: + +```java +try{ +// 可能发生异常的代码 +}catch (exception(type) e(object)){ +// 异常处理代码 +} +``` + +一个 `try` 块后面可以跟多个 `catch` 块,用来捕获不同类型的异常并做相应的处理,当 try 块中的某一行代码发生异常时,之后的代码就不再执行,而是会跳转到异常对应的 catch 块中执行。 + +如果一个 try 块后面跟了多个与之关联的 catch 块,那么应该把特定的异常放在前面,通用型的异常放在后面,不然编译器会提示错误。举例来说。 + +```java +static void test() { + int num1, num2; + try { + num1 = 0; + num2 = 62 / num1; + System.out.println(num2); + System.out.println("try 块的最后一句"); + } catch (ArithmeticException e) { + // 算术运算发生时跳转到这里 + System.out.println("除数不能为零"); + } catch (Exception e) { + // 通用型的异常意味着可以捕获所有的异常,它应该放在最后面, + System.out.println("异常发生了"); + } + System.out.println("try-catch 之外的代码."); +} +``` + +“为什么 Exception 不能放到 ArithmeticException 前面呢?”三妹问。 + +“因为 ArithmeticException 是 Exception 的子类,它更具体,我们看到就这个异常就知道是发生了算术错误,而 Exception 比较泛,它隐藏了具体的异常信息,我们看到后并不确定到底是发生了哪一种类型的异常,对错误的排查很不利。”我说,“再者,如果把通用型的异常放在前面,就意味着其他的 catch 块永远也不会执行,所以编译器就直接提示错误了。” + +“再给你举个例子,注意看,三妹。” + +```java +static void test1 () { + try{ + int arr[]=new int[7]; + arr[4]=30/0; + System.out.println("try 块的最后"); + } catch(ArithmeticException e){ + System.out.println("除数必须是 0"); + } catch(ArrayIndexOutOfBoundsException e){ + System.out.println("数组越界了"); + } catch(Exception e){ + System.out.println("一些其他的异常"); + } + System.out.println("try-catch 之外"); +} +``` + +这段代码在执行的时候,第一个 catch 块会执行,因为除数为零;我再来稍微改动下代码。 + +```java +static void test1 () { + try{ + int arr[]=new int[7]; + arr[9]=30/1; + System.out.println("try 块的最后"); + } catch(ArithmeticException e){ + System.out.println("除数必须是 0"); + } catch(ArrayIndexOutOfBoundsException e){ + System.out.println("数组越界了"); + } catch(Exception e){ + System.out.println("一些其他的异常"); + } + System.out.println("try-catch 之外"); +} +``` + +“我知道,二哥,第二个 catch 块会执行,因为没有发生算术异常,但数组越界了。”三妹没等我把代码运行起来就说出了答案。 + +“三妹,你说得很对,我再来改一下代码。” + +```java +static void test1 () { + try{ + int arr[]=new int[7]; + arr[9]=30/1; + System.out.println("try 块的最后"); + } catch(ArithmeticException | ArrayIndexOutOfBoundsException e){ + System.out.println("除数必须是 0"); + } + System.out.println("try-catch 之外"); +} +``` + +“当有多个 catch 的时候,也可以放在一起,用竖划线 `|` 隔开,就像上面这样。”我说。 + +“这样不错呀,看起来更简洁了。”三妹说。 + +`finally` 块的语法也不复杂。 + +```java +try { + // 可能发生异常的代码 +}catch { + // 异常处理 +}finally { + // 必须执行的代码 +} +``` + +在没有 `try-with-resources` 之前,finally 块常用来关闭一些连接资源,比如说 socket、数据库链接、IO 输入输出流等。 + +```java +OutputStream osf = new FileOutputStream( "filename" ); +OutputStream osb = new BufferedOutputStream(opf); +ObjectOutput op = new ObjectOutputStream(osb); +try{ + output.writeObject(writableObject); +} finally{ + op.close(); +} +``` + +“三妹,注意,使用 finally 块的时候需要遵守这些规则。” + +- finally 块前面必须有 try 块,不要把 finally 块单独拉出来使用。编译器也不允许这样做。 +- finally 块不是必选项,有 try 块的时候不一定要有 finally 块。 +- 如果 finally 块中的代码可能会发生异常,也应该使用 try-catch 进行包裹。 +- 即便是 try 块中执行了 return、break、continue 这些跳转语句,finally 块也会被执行。 + +“真的吗,二哥?”三妹对最后一个规则充满了疑惑。 + +“来试一下就知道了。”我说。 + +```java +static int test2 () { + try { + return 112; + } + finally { + System.out.println("即使 try 块有 return,finally 块也会执行"); + } +} +``` + +来看一下输出结果: + +``` +即使 try 块有 return,finally 块也会执行 +``` + +“那,会不会有不执行 finally 的情况呀?”三妹很好奇。 + +“有的。”我斩钉截铁地回答。 + +- 遇到了死循环。 +- 执行了 `System. exit()` 这行代码。 + +`System.exit()` 和 `return` 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。 + +“三妹,来看一下源码的文档注释就全明白了!” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/exception/try-catch-finally-01.png) + +至于参数 status 的值也很好理解,如果是异常退出,设置为非 0 即可,通常用 1 来表示;如果是想正常退出程序,用 0 表示即可。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/exception/npe.md b/docs/exception/npe.md similarity index 89% rename from docs/src/exception/npe.md rename to docs/exception/npe.md index 1f86928b00..c43debd280 100644 --- a/docs/src/exception/npe.md +++ b/docs/exception/npe.md @@ -1,17 +1,11 @@ --- -title: Java空指针NullPointerException的传说 -shortTitle: 空指针的传说 category: - Java核心 tag: - - 异常处理 -description: 本文详细解析了Java编程中的空指针异常(NullPointerException)现象,从产生原因、常见场景到预防方法,为您揭开NullPointerException的神秘面纱。文章还提供了实际代码示例,帮助您理解如何在实际开发中避免空指针异常,提高代码的健壮性和可靠性。 -head: - - - meta - - name: keywords - content: java,npe,NullPointerException + - Java --- +# Java空指针NullPointerException的传说 **空指针**,号称天下最强刺客。 @@ -21,13 +15,13 @@ head: ... ... -我叫王二,我来到这个奇怪的世界已经一年了,我等了一年,穿越附赠的老爷爷、戒指、系统什么的我到现在都没发现。 +我叫铁柱,我来到这个奇怪的世界已经一年了,我等了一年,穿越附赠的老爷爷、戒指、系统什么的我到现在都没发现。 而且这个世界看起来也太奇怪了,这里好像叫什么 **Java** 大陆,我只知道这个世界的最强者叫做 **Object**,听说是什么道祖级的存在,我也不知道是什么意思,毕竟我现在好像还是个菜鸡,别的主角一年都应该要飞升仙界了吧,我还连个小火球都放不出来。 哦,对了,上面的那段话是我在茶馆喝茶的时候听说书的先生说的,总觉得空指针这个名字怪怪的,好像在什么地方听说过。 -我的头痛的毛病又犯了,我已经记不起来我为什么来到这里了,我只记得我的名字叫王二,其他的,我只感觉这个奇怪的世界有一种熟悉,但是我什么都记不起来了。 +我的头痛的毛病又犯了,我已经记不起来我为什么来到这里了,我只记得我的名字叫铁柱,其他的,我只感觉这个奇怪的世界有一种熟悉,但是我什么都记不起来了。 算了,得过且过吧。 @@ -177,7 +171,7 @@ Object听到这话,皱了皱眉,他沉默了一会儿,缓缓站起身子 我见他好像魔怔了,仿佛在思考什么,于是迈步走到他刚才站立的地方看着前面,原来,这是他们的族谱!这里是异常的祠堂! -![](https://cdn.paicoding.com/tobebetterjavaer/images/exception/npe-1.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/exception/npe-1.jpg) 看完这张族谱,我恍然大悟,好像明白了什么。突然,我的脑袋里出现了一个冰冷的机器声音:“获取异常族谱,历练完成度+100。” @@ -223,14 +217,7 @@ Object听到这话,皱了皱眉,他沉默了一会儿,缓缓站起身子 可是,他为什么要给我,看他刚才的样子都想打我了,又突然给了我这些?还有他一直在说的规则之力又是什么?这座城市为什么又这么诡异? ->转载链接:[https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg](https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg),作者:艾小仙 +>转载链接:https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/exception/shijian.md b/docs/exception/shijian.md new file mode 100644 index 0000000000..9150539c70 --- /dev/null +++ b/docs/exception/shijian.md @@ -0,0 +1,219 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java异常处理的20个最佳实践 + +“三妹啊,今天我来给你传授几个异常处理的最佳实践经验,以免你以后在开发中采坑。”我面带着微笑对三妹说。 + +“好啊,二哥,我洗耳恭听。”三妹也微微一笑,欣然接受。 + +“好,那哥就不废话了。开整。” + +-------- + +**1)尽量不要捕获 RuntimeException** + +阿里出品的嵩山版 Java 开发手册上这样规定: + +>尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。 + +正例: + +```java +if (obj != null) { + //... +} +``` + +反例: + +```java +try { + obj.method(); +} catch (NullPointerException e) { + //... +} +``` + +“哦,那如果有些异常预检查不出来呢?”三妹问。 + +“的确会存在这样的情况,比如说 NumberFormatException,虽然也属于 RuntimeException,但没办法预检查,所以还是应该用 catch 捕获处理。”我说。 + +**2)尽量使用 try-with-resource 来关闭资源** + +当需要关闭资源时,尽量不要使用 try-catch-finally,禁止在 try 块中直接关闭资源。 + +反例: + +```java +public void doNotCloseResourceInTry() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + inputStream.close(); + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} +``` + +“为什么呢?”三妹问。 + +“原因也很简单,因为一旦 `close()` 之前发生了异常,那么资源就无法关闭。直接使用 [try-with-resource](https://mp.weixin.qq.com/s/7yhHOG0SVCfoHdhtZHfeVg) 来处理是最佳方式。”我说。 + +```java +public void automaticallyCloseResource() { + File file = new File("./tmp.txt"); + try (FileInputStream inputStream = new FileInputStream(file);) { + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} +``` + +“除非资源没有实现 AutoCloseable 接口。”我补充道。 + +“那这种情况下怎么办呢?”三妹问。 + +“就在 finally 块关闭流。”我说。 + +```java +public void closeResourceInFinally() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + } catch (FileNotFoundException e) { + log.error(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + log.error(e); + } + } + } +} +``` + +**3)不要捕获 Throwable** + +Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了。 + +```java +public void doNotCatchThrowable() { + try { + } catch (Throwable t) { + // 不要这样做 + } +} +``` + +“到底为什么啊?”三妹问。 + +“因为有些 error 是不需要程序来处理,程序可能也处理不了,比如说 OutOfMemoryError 或者 StackOverflowError,前者是因为 Java 虚拟机无法申请到足够的内存空间时出现的非正常的错误,后者是因为线程申请的栈深度超过了允许的最大深度出现的非正常错误,如果捕获了,就掩盖了程序应该被发现的严重错误。”我说。 + +“打个比方,一匹马只能拉一车厢的货物,拉两车厢可能就挂了,但一 catch,就发现不了问题了。”我补充道。 + +**4)不要省略异常信息的记录** + +很多时候,由于疏忽大意,开发者很容易捕获了异常却没有记录异常信息,导致程序上线后真的出现了问题却没有记录可查。 + +```java +public void doNotIgnoreExceptions() { + try { + } catch (NumberFormatException e) { + // 没有记录异常 + } +} +``` + +应该把错误信息记录下来。 + +```java +public void logAnException() { + try { + } catch (NumberFormatException e) { + log.error("哦,错误竟然发生了: " + e); + } +} +``` + +**5)不要记录了异常又抛出了异常** + +这纯属画蛇添足,并且容易造成错误信息的混乱。 + +反例: + +```java +try { +} catch (NumberFormatException e) { + log.error(e); + throw e; +} +``` + +要抛出就抛出,不要记录,记录了又抛出,等于多此一举。 + +反例: + +```java +public void wrapException(String input) throws MyBusinessException { + try { + } catch (NumberFormatException e) { + throw new MyBusinessException("错误信息描述:", e); + } +} +``` + +这种也是一样的道理,既然已经捕获了,就不要在方法签名上抛出了。 + +**6)不要在 finally 块中使用 return** + +阿里出品的嵩山版 Java 开发手册上这样规定: + +>try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。 + +反例: + +```java +private int x = 0; +public int checkReturn() { + try { + return ++x; + } finally { + return ++x; + } +} +``` + +“哦,确实啊,try 块中 x 返回的值为 1,到了 finally 块中就返回 2 了。”三妹说。 + +“是这样的。”我点点头。 + +---------- + +“好了,三妹,关于异常处理实践就先讲这 6 条吧,实际开发中你还会碰到其他的一些坑,自己踩一踩可能印象更深刻一些。”我说。 + +“那万一到时候我工作后被领导骂了怎么办?”三妹委屈地说。 + +“新人嘛,总要写几个 bug 才能对得起新人这个称号嘛。”我轻描淡写地说。 + +“好吧。”三妹无奈地叹了口气。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + + + + diff --git a/docs/exception/try-with-resouces.md b/docs/exception/try-with-resouces.md new file mode 100644 index 0000000000..51f5f26a68 --- /dev/null +++ b/docs/exception/try-with-resouces.md @@ -0,0 +1,301 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 详解Java7新增的try-with-resouces语法 + + +“二哥,终于等到你讲 try-with-resouces 了!”三妹夸张的表情让我有些吃惊。 + +“三妹,不要激动呀!开讲之前,我们还是要来回顾一下 try–catch-finally,好做个铺垫。”我说,“来看看这段代码吧。” + +```java +public class TrycatchfinallyDecoder { + public static void main(String[] args) { + BufferedReader br = null; + try { + String path = TrycatchfinallyDecoder.class.getResource("/牛逼.txt").getFile(); + String decodePath = URLDecoder.decode(path,"utf-8"); + br = new BufferedReader(new FileReader(decodePath)); + + String str = null; + while ((str =br.readLine()) != null) { + System.out.println(str); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} +``` + +“我简单来解释下。”等三妹看完这段代码后,我继续说,“在 try 块中读取文件中的内容,并一行一行地打印到控制台。如果文件找不到或者出现 IO 读写错误,就在 catch 中捕获并打印错误的堆栈信息。最后,在 finally 中关闭缓冲字符读取器对象 BufferedReader,有效杜绝了资源未被关闭的情况下造成的严重性能后果。” + +“在 Java 7 之前,try–catch-finally 的确是确保资源会被及时关闭的最佳方法,无论程序是否会抛出异常。” + +三妹点了点头,表示同意。 + +“不过,这段代码还是有些臃肿,尤其是 finally 中的代码。”我说,“况且,try–catch-finally 至始至终存在一个严重的隐患:try 中的 `br.readLine()` 有可能会抛出 `IOException`,finally 中的 `br.close()` 也有可能会抛出 `IOException`。假如两处都不幸地抛出了 IOException,那程序的调试任务就变得复杂了起来,到底是哪一处出了错误,就需要花一番功夫,这是我们不愿意看到的结果。” + +“我来给你演示下,三妹。” + +“首先,我们来定义这样一个类 MyfinallyReadLineThrow,它有两个方法,分别是 `readLine()` 和 `close()`,方法体都是主动抛出异常。” + +```java +class MyfinallyReadLineThrow { + public void close() throws Exception { + throw new Exception("close"); + } + + public void readLine() throws Exception { + throw new Exception("readLine"); + } +} +``` + +“然后在 `main()` 方法中使用 try-catch-finally 的方式调用 MyfinallyReadLineThrow 的 `readLine()` 和 `close()` 方法。” + +```java +public class TryfinallyCustomReadLineThrow { + public static void main(String[] args) throws Exception { + MyfinallyReadLineThrow myThrow = null; + try { + myThrow = new MyfinallyReadLineThrow(); + myThrow.readLine(); + } finally { + myThrow.close(); + } + } +} +``` + +运行上述代码后,错误堆栈如下所示: + +``` +Exception in thread "main" java.lang.Exception: close + at com.cmower.dzone.trycatchfinally.MyfinallyOutThrow.close(TryfinallyCustomOutThrow.java:17) + at com.cmower.dzone.trycatchfinally.TryfinallyCustomOutThrow.main(TryfinallyCustomOutThrow.java:10) +``` + +“看出来问题了吗,三妹?” + +“啊?`readLine()` 方法的异常信息竟然被 `close()` 方法的堆栈信息吃了!” + +“不错啊,三妹,火眼金睛,的确,这会让我们误以为要调查的目标是 `close()` 方法而不是 `readLine()` 方法——尽管它也是应该怀疑的对象。” + +“但有了 try-with-resources 后,这些问题就迎刃而解了。前提条件只有一个,就是需要释放的资源(比如 BufferedReader)实现了 AutoCloseable 接口。” + +```java +try (BufferedReader br = new BufferedReader(new FileReader(decodePath));) { + String str = null; + while ((str =br.readLine()) != null) { + System.out.println(str); + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + +“你瞧,三妹,finally 块消失了,取而代之的是把要释放的资源写在 try 后的 `()` 中。如果有多个资源(BufferedReader 和 PrintWriter)需要释放的话,可以直接在 `()` 中添加。” + +```java +try (BufferedReader br = new BufferedReader(new FileReader(decodePath)); + PrintWriter writer = new PrintWriter(new File(writePath))) { + String str = null; + while ((str =br.readLine()) != null) { + writer.print(str); + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + +“如果想释放自定义资源的话,只要让它实现 AutoCloseable 接口,并提供 `close()` 方法即可。” + +```java +public class TrywithresourcesCustom { + public static void main(String[] args) { + try (MyResource resource = new MyResource();) { + } catch (Exception e) { + e.printStackTrace(); + } + } +} + +class MyResource implements AutoCloseable { + @Override + public void close() throws Exception { + System.out.println("关闭自定义资源"); + } +} +``` + +来看一下代码运行后的输出结果: + +``` +关闭自定义资源 +``` + +“好神奇呀!”三妹欣喜若狂,“在 `try ()` 中只是 new 了一个 MyResource 的对象,其他什么也没干,`close()` 方法就执行了!” + +“想知道为什么吗?三妹。” + +“当然想啊。” + +“来看看反编译后的字节码吧。” + +```java +class MyResource implements AutoCloseable { + MyResource() { + } + + public void close() throws Exception { + System.out.println("关闭自定义资源"); + } +} + +public class TrywithresourcesCustom { + public TrywithresourcesCustom() { + } + + public static void main(String[] args) { + try { + MyResource resource = new MyResource(); + resource.close(); + } catch (Exception var2) { + var2.printStackTrace(); + } + + } +} +``` + +“啊,原来如此。编译器主动为 try-with-resources 进行了变身,在 try 中调用了 `close()` 方法。” + +“是这样的。接下来,我们在 `MyResourceOut` 类中再添加一个 `out()` 方法。” + +```java +class MyResourceOut implements AutoCloseable { + @Override + public void close() throws Exception { + System.out.println("关闭自定义资源"); + } + + public void out() throws Exception{ + System.out.println("沉默王二,一枚有趣的程序员"); + } +} +``` + +“这次,我们在 try 中调用一下 `out()` 方法。” + +```java +public class TrywithresourcesCustomOut { + public static void main(String[] args) { + try (MyResourceOut resource = new MyResourceOut();) { + resource.out(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +“再来看一下反编译的字节码。” + +``` +public class TrywithresourcesCustomOut { + public TrywithresourcesCustomOut() { + } + + public static void main(String[] args) { + try { + MyResourceOut resource = new MyResourceOut(); + + try { + resource.out(); + } catch (Throwable var5) { + try { + resource.close(); + } catch (Throwable var4) { + var5.addSuppressed(var4); + } + + throw var5; + } + + resource.close(); + } catch (Exception var6) { + var6.printStackTrace(); + } + + } +} +``` + +“这次,`catch` 块主动调用了 `resource.close()`,并且有一段很关键的代码 ` var5.addSuppressed(var4)`。” + +“这是为了什么呢?”三妹问。 + +“当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过 `addSuppressed()` 方法把这些被抑制的方法记录下来,然后被抑制的异常就会出现在抛出的异常的堆栈信息中,可以通过 `getSuppressed()` 方法来获取这些异常。这样做的好处是不会丢失任何异常,方便我们进行调试。”我说。 + +“有没有想到之前的那个例子——在 try-catch-finally 中,`readLine()` 方法的异常信息竟然被 `close()` 方法的堆栈信息吃了。现在有了 try-with-resources,再来看看和 `readLine()` 方法一致的 `out()` 方法会不会被 `close()` 吃掉吧。” + +```java +class MyResourceOutThrow implements AutoCloseable { + @Override + public void close() throws Exception { + throw new Exception("close()"); + } + + public void out() throws Exception{ + throw new Exception("out()"); + } +} +``` + +“调用这 2 个方法。” + +```java +public class TrywithresourcesCustomOutThrow { + public static void main(String[] args) { + try (MyResourceOutThrow resource = new MyResourceOutThrow();) { + resource.out(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +“程序输出的结果如下所示。” + +``` +java.lang.Exception: out() + at com.cmower.dzone.trycatchfinally.MyResourceOutThrow.out(TrywithresourcesCustomOutThrow.java:20) + at com.cmower.dzone.trycatchfinally.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:6) + Suppressed: java.lang.Exception: close() + at com.cmower.dzone.trycatchfinally.MyResourceOutThrow.close(TrywithresourcesCustomOutThrow.java:16) + at com.cmower.dzone.trycatchfinally.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:5) +``` + +“瞧,这次不会了,`out()` 的异常堆栈信息打印出来了,并且 `close()` 方法的堆栈信息上加了一个关键字 `Suppressed`,一目了然。” + +“三妹,怎么样?是不是感觉 try-with-resouces 好用多了!我来简单总结下哈,在处理必须关闭的资源时,始终有限考虑使用 try-with-resources,而不是 try–catch-finally。前者产生的代码更加简洁、清晰,产生的异常信息也更靠谱。” + +“靠谱!”三妹说。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + diff --git a/docs/src/git/git-qiyuan.md b/docs/git/git-qiyuan.md similarity index 87% rename from docs/src/git/git-qiyuan.md rename to docs/git/git-qiyuan.md index ed60af3df6..10f83e0103 100644 --- a/docs/src/git/git-qiyuan.md +++ b/docs/git/git-qiyuan.md @@ -1,24 +1,24 @@ --- -title: 1小时彻底掌握 Git,(可能是)史上最简单明了的 Git 教程 -shortTitle: 最简单明了的 Git 教程 category: - - 开发/构建工具 + - Java企业级开发 tag: - Git -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,1小时彻底掌握 Git,(可能是)史上最简单明了的 Git 教程 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Git入门,Git教程,git --- +# 我在工作中是如何使用 Git 的 + ## 一、Git 起源 Git 是一个分布式版本控制系统,缔造者是大名鼎鼎的林纳斯·托瓦茲 (Linus Torvalds),Git 最初的目的是为了能更好的管理 Linux 内核源码。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-01.png) + +PS:**为了能够帮助更多的 Java 爱好者,已将《Java 程序员进阶之路》开源到了 GitHub(本篇已收录)。如果你也喜欢这个专栏,觉得有帮助的话,可以去点个 star,这样也方便以后进行更系统化的学习**: +[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + +*每天看着 star 数的上涨我心里非常的开心,希望越来越多的 Java 爱好者能因为这个开源项目而受益,而越来越多人的 star,也会激励我继续更新下去*~ 大家都知道,Linux 内核是开源的,参与者众多,到目前为止,共有两万多名开发者给 Linux Kernel 提交过代码。 @@ -54,7 +54,7 @@ Junio Hamano 觉得 Linus 设计的这些命令对于普通用户不太友好, 如今,Git 已经成为全球软件开发者的标配。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-02.png) 原本的 Git 只适用于 Unix/Linux 平台,但随着 Cygwin、msysGit 环境的成熟,以及 TortoiseGit 这样易用的GUI工具,Git 在 Windows 平台下也逐渐成熟。 @@ -65,7 +65,7 @@ Junio Hamano 觉得 Linus 设计的这些命令对于普通用户不太友好, Git 和传统的版本控制工具 CVS、SVN 有不小的区别,前者关心的是文件的整体性是否发生了改变,后两者更关心文件内容上的差异。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-03.png) 除此之外,Git 更像是一个文件系统,每个使用它的主机都可以作为版本库,并且不依赖于远程仓库而离线工作。开发者在本地就有历史版本的副本,因此就不用再被远程仓库的网络传输而束缚。 @@ -88,7 +88,7 @@ Git 中的绝大多数操作都只需要访问本地文件和资源,一般不 Git 的工作流程是这样的: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-04.png) - 在工作目录中修改文件 @@ -102,11 +102,11 @@ Git 的工作流程是这样的: >https://git-scm.com/downloads -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-05.png) 我个人使用的 macOS 系统,可以直接使用 `brew install git` 命令安装,非常方便。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/git-qiyuan-06.png) 安装成功后,再使用 `git --version` 就可以查看版本号了,我本机上安装的是 2.23.0 版本。 @@ -287,7 +287,7 @@ TREE: 目录树对象。在 Linus 的设计里,TREE 对象就是一个时间 另外,由于 TREE 上记录文件名及属性信息,对于修改文件属性或修改文件名、移动目录而不修改文件内容的情况,可以复用 BLOB 对象,节省存储资源。而 Git 在后来的开发演进中又优化了 TREE 的设计,变成了某一时间点文件夹信息的抽象,TREE 包含其子目录的 TREE 的对象信息(SHA1)。这样,对于目录结构很复杂或层级较深的 Git 库 可以节约存储资源。历史信息被记录在第三种对象 CHANGESET 里。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/neibushixian-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/neibushixian-01.png) CHANGESET:即 Commit 对象。一个 CHANGESET 对象中记录了该次提交的 TREE 对象信息(SHA1),以及提交者(committer)、提交备注(commit message)等信息。 @@ -304,7 +304,7 @@ Linus 解释了“当前目录缓存”的设计,该缓存就是一个二进 - 1. 能够快速的复原缓存的完整内容,即使不小心把当前工作区的文件删除了,也可以从缓存中恢复所有文件; - 2. 能够快速找出缓存中和当前工作区内容不一致的文件。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/neibushixian-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/neibushixian-02.png) Linus 在 Git 的第一次代码提交里便完成了 Git 的最基础功能,并可以编译使用。代码极为简洁,加上 Makefile 一共只有 848 行。感兴趣的话可以通过上一段所述方法 checkout Git 最早的 commit 上手编译玩玩,只要有 Linux 环境即可。 @@ -332,7 +332,7 @@ Linus 在 Git 的第一次代码提交里便完成了 Git 的最基础功能, 一般来说,日常使用只要记住下图中这 6 个命令就可以了,但是熟练使用 Git,恐怕要记住60~100个命令~ -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/mingling-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/mingling-01.png) @@ -347,7 +347,7 @@ Linus 在 Git 的第一次代码提交里便完成了 Git 的最基础功能, 下面是阮一峰老师整理的常用 Git 命令清单,有必要的话,可以打印一份出来,放在工作台~ ->[http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html](http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html) +>http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html ### 1、新建代码库 @@ -641,7 +641,7 @@ $ git archive 但是,太方便了也会产生副作用。如果你不加注意,很可能会留下一个枝节蔓生、四处开放的版本库,到处都是分支,完全看不出主干发展的脉络。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-01.png) 那有没有一个好的分支策略呢?答案当然是有的。 @@ -650,7 +650,7 @@ $ git archive 首先,代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-02.png) Git主分支的名字,默认叫做Master。它是自动建立的,版本库初始化以后,默认就是在主分支在进行开发。 @@ -658,7 +658,7 @@ Git主分支的名字,默认叫做Master。它是自动建立的,版本库 主分支只用来发布重大版本,日常开发应该在另一条分支上完成。我们把开发用的分支,叫做Develop。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-03.png) 这个分支可以用来生成代码的最新隔夜版本(nightly)。如果想正式对外发布,就在Master分支上,对Develop分支进行"合并"(merge)。 @@ -680,11 +680,11 @@ Git创建Develop分支的命令: 这里稍微解释一下上一条命令的--no-ff参数是什么意思。默认情况下,Git执行"快进式合并"(fast-farward merge),会直接将Master分支指向Develop分支。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-04.png) 使用--no-ff参数后,会执行正常合并,在Master分支上生成一个新节点。为了保证版本演进的清晰,我们希望采用这种做法。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-05.png) ### 3、临时性分支 @@ -702,7 +702,7 @@ Git创建Develop分支的命令: **第一种是功能分支**,它是为了开发某种特定功能,从Develop分支上面分出来的。开发完成后,要再并入Develop。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-06.png) 功能分支的名字,可以采用feature-*的形式命名。 @@ -755,7 +755,7 @@ Git创建Develop分支的命令: 修补bug分支是从Master分支上面分出来的。修补结束以后,再合并进Master和Develop分支。它的命名,可以采用fixbug-*的形式。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/fenzhi-07.png) 创建一个修补bug分支: ``` @@ -791,7 +791,7 @@ Git创建Develop分支的命令: 新建一个文件夹,比如说 testgit,然后使用 `git init` 命令就可以把这个文件夹初始化为 Git 仓库了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-01.png) 初始化Git 仓库成功后,可以看到多了一个 .git 的目录,没事不要乱动,免得破坏了 Git 仓库的结构。 @@ -802,17 +802,17 @@ Git创建Develop分支的命令: 第二步,使用 `git commit` 命令告诉 Git,把文件提交到仓库。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-02.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-02.png) 可以使用 `git status` 来查看是否还有文件未提交。 也可以在文件中新增一行内容“传统美德不能丢,记得点赞哦~”,再使用 `git status` 来查看结果。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-03.png) 如果想查看文件到底哪里做了修改,可以使用 `git diff` 命令: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-04.png) 确认修改的内容后,可以使用 `git add` 和 `git commit` 再次提交。 @@ -822,11 +822,11 @@ Git创建Develop分支的命令: 现在我已经对 readme.txt 文件做了三次修改了。可以通过 `git log` 命令来查看历史记录: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-05.png) 也可以通过 `gitk` 这个命令来启动图形化界面来查看版本历史。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-06.png) 如果想回滚的话,比如说回滚到上一个版本,可以执行以下两种命令: @@ -835,15 +835,15 @@ Git创建Develop分支的命令: 2)`git reset --hard HEAD~100`,如果回滚到前 100 个版本,用这个命令比上一个命令更方便。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-07.png) 那假如回滚错了,想恢复,不记得版本号了,可以先执行 `git reflog` 命令查看版本号: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-08.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-08.png) 然后再通过 `git reset --hard` 命令来恢复: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-09.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-09.png) ### 3、工作区和暂存区的区别 @@ -865,7 +865,7 @@ Git 在提交文件的时候分两步,第一步 `git add` 命令是把文件 原子性带来的好处是显而易见的,这使得我们可以把项目整体还原到某个时间点,就这一点,SVN 就完虐 CVS 这些代码版本管理系统了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-10.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-10.png) Git 作为逼格最高的代码版本管理系统,自然要借鉴 SVN 这个优良特性的。但不同于 SVN 的是,Git 一开始搞的都是命令行,没有图形化界面,如果想要像 SVN 那样一次性选择多个文件或者不选某些文件(见上图),还真特喵的是个麻烦事。 @@ -881,7 +881,7 @@ Git 作为逼格最高的代码版本管理系统,自然要借鉴 SVN 这个 我们先用 `git status` 命令查看一下状态,再用 `git add` 将文件添加到暂存区,最后再用 `git commit` 一次性提交到 Git 仓库。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-11.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-11.png) ### 4、撤销修改 @@ -895,17 +895,17 @@ Git 作为逼格最高的代码版本管理系统,自然要借鉴 SVN 这个 答案当然是有了,其实在我们执行 `git status` 命令查看 Git 状态的时候,结果就提示我们可以使用 `git restore` 命令来撤销这次操作的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-12.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-12.png) 那其实在 git version 2.23.0 版本之前,是可以通过 `git checkout` 命令来完成撤销操作的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-13.png) checkout 可以创建分支、导出分支、切换分支、从暂存区删除文件等等,一个命令有太多功能就容易让人产生混淆。2.23.0 版本改变了这种混乱局面,git switch 和 git restore 两个新的命令应运而生。 switch 专注于分支的切换,restore 专注于撤销修改。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-14.png) ### 5、远程仓库 @@ -923,7 +923,7 @@ Git 是一款分布式版本控制系统,所以同一个 Git 仓库,可以 **第一步,通过 `ls -al ~/.ssh` 命令检查 SSH 密钥是否存在** -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-15.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-15.png) 如果没有 id_rsa.pub、id_ecdsa.pub、id_ed25519.pub 这 3 个文件,表示密钥不存在。 @@ -932,26 +932,26 @@ Git 是一款分布式版本控制系统,所以同一个 Git 仓库,可以 执行以下命令,注意替换成你的邮箱: ``` -ssh-keygen -t ed25519 -C "your_email@example.com" +ssh-keygen -t ed25519 -C "your_email@example.com ``` 然后一路回车: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-16.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-16.png) 记得复制一下密钥,在 id_ed25519.pub 文件中: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-17.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-17.png) **第三步,添加 SSH 密钥到 GitHub 帐户** 在个人账户的 settings 菜单下找到 SSH and GPG keys,将刚刚复制的密钥添加到 key 这一栏中,点击「add SSH key」提交。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-18.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-18.png) Title 可不填写,提交成功后会列出对应的密钥: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-19.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-19.png) **为什么 GitHub 需要 SSH 密钥呢**? @@ -961,17 +961,17 @@ Title 可不填写,提交成功后会列出对应的密钥: 点击新建仓库,填写仓库名称等信息: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-20.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-20.png) **第五步,把本地仓库同步到 GitHub** 复制远程仓库的地址: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-21.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-21.png) 在本地仓库中执行 `git remote add` 命令将 GitHub 仓库添加到本地: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-22.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-22.png) 当我们第一次使用Git 的 push 命令连接 GitHub 时,会得到一个警告⚠️: @@ -985,28 +985,28 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 接下来,我们使用 `git push` 命令将当前本地分支推送到 GitHub。加上了 -u 参数后,Git 不但会把本地的 master 分支推送的远程 master 分支上,还会把本地的 master 分支和远程的master 分支关联起来,在以后的推送或者拉取时就可以简化命令(比如说 `git push github master`)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-23.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-23.png) 此时,我们刷一下 GitHub,可以看到多了一个 master 分支,并且本地的两个文件都推送成功了! -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-24.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-24.png) 从现在开始,只要本地做了修改,就可以通过 `git push` 命令推送到 GitHub 远程仓库了。 还可以使用 `git clone` 命令将远程仓库拷贝到本地。比如说我现在有一个 3.4k star 的仓库 JavaBooks, -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-25.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-25.png) 然后我使用 `git clone` 命令将其拷贝到本地。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-26.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/jibenshiyong-26.png) ## 八、详解 sparse-checkout 命令 -前天不是搭建了一个《二哥的Java进阶之路》的网站嘛,其中用到了 Git 来作为云服务器和 GitHub 远程仓库之间的同步工具。 +前天不是搭建了一个《Java 程序员进阶之路》的网站嘛,其中用到了 Git 来作为云服务器和 GitHub 远程仓库之间的同步工具。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-01.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-01.png) @@ -1017,9 +1017,9 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes ### 1、使用 Git 中遇到的一个大麻烦 -首先给大家通报一下,一天前[上线的《二哥的Java进阶之路》网站](https://javabetter.cn),目前访问次数已经突破 1000 了。 +首先给大家通报一下,一天前[上线的《Java 程序员进阶之路》网站](https://tobebetterjavaer.com),目前访问次数已经突破 1000 了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-03.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-03.png) 正所谓**不积跬步无以至千里,不积小流无以成江海**。 @@ -1034,7 +1034,7 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 大家可以先看一下我这个 GitHub 仓库的目录结构哈。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-04.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-04.png) - docs 是文档目录,里面是 md 文件,所有的教程原稿都在这里。 - codes 是代码目录,里面是教程的配套源码。 @@ -1042,11 +1042,11 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 这样就可以利用 GitHub 来做免费的图床,并且还可以白票 jsDelivr CDN 的全球加速,简直不要太爽! -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-05.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-05.png) 比如说 images 目录下有一张 logo 图 logo-01.png: -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-06.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-06.png) 如果使用 GitHub 仓库的原始路径来访问的话,速度贼慢! @@ -1054,7 +1054,7 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 使用 jsDelivr 加速后就不一样了,速度飞起! ->https://cdn.paicoding.com/tobebetterjavaer/images/logo-01.png +>http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/logo-01.png 简单总结下 GitHub 作为图床的正确用法,就两条: @@ -1064,7 +1064,7 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 付费七牛云或者阿里云图床的小伙伴不妨试试这种方式,能白票咱绝不花一分冤枉钱。 -那也就是说,《二哥的Java进阶之路》网站上的图片都是通过 GitHub 图床加载的,不需要将图片从 GitHub 仓库拉取到云服务器上。要知道,一台云服务器的空间是极其昂贵的,能省的空间咱必须得省。 +那也就是说,《Java 程序员进阶之路》网站上的图片都是通过 GitHub 图床加载的,不需要将图片从 GitHub 仓库拉取到云服务器上。要知道,一台云服务器的空间是极其昂贵的,能省的空间咱必须得省。 ### 2、学习 Git 中遇到的一个大惊喜 @@ -1074,16 +1074,16 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 最后还是浏览 Git 官方手册(也可以看[Pro Git](https://mp.weixin.qq.com/s/RpFzXOa2VlFNd7ylLmr9LQ))才找到了一个牛逼的命令:**git sparse-checkout,它可以帮助我们在拉取远程仓库的时候只同步那些我们想要的目录和文件**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-07.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-07.png) 具体怎么用,可以看官方文档: ->[https://git-scm.com/docs/git-sparse-checkout](https://git-scm.com/docs/git-sparse-checkout) +>https://git-scm.com/docs/git-sparse-checkout 但没必要,hhhh,我们直接实战。 第一步,通过 `git remote add -f orgin git@github.com:itwanger/toBeBetterJavaer.git` 命令从 GitHub 上拉取仓库。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-08.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-08.png) 第二步,启用 sparse-checkout,并初始化 @@ -1091,30 +1091,30 @@ Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 然后再执行 `git sparse-checkout init` 初始化。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-09.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-09.png) 第三步,使用 sparse-checkout 来拉取我们想要的仓库目录 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-10.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-10.png) 比如说,我们只想拉取 docs 目录,可以执行 `git sparse-checkout set docs` 命令。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-11.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-11.png) 如果是第一次使用 sparse-checkout 的话,还需要执行一下 `git pull orgin master` 命令拉取一次。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-12.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-12.png) 第四步,验证是否生效 可以执行 `ls -al` 命令来确认 sparse-checkout 是否生效。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-13.png) 如图所示,确实只拉取到了 docs 目录。 假如还想要拉取其他文件或者目录的话,可以通过 `git sparse-checkout add` 命令来添加。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-14.png) 这就实现了,**远程仓库和云服务器仓库之间的定制化同步,需要什么目录和文件就同步什么目录和文件,不需要的可以统统不要**。 @@ -1122,13 +1122,13 @@ GitHub 仓库可以免费用,空间也无限大,但云服务可是要抠抠 我对比了一下,远程仓库大概 145 M,图片就占了 72 M,妥妥地省下了一半的存储空间。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-15.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-15.png) 如何禁用 git sparse-checkout 呢? 也简单,只需要执行一下 `git sparse-checkout disable` 命令就可以了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-16.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-16.png) 可以看到,那些我们不想要的目录和文件统统都又回来了。 @@ -1136,7 +1136,7 @@ GitHub 仓库可以免费用,空间也无限大,但云服务可是要抠抠 也简单,只需要执行一下 `git sparse-checkout reapply` 命令就可以了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-17.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-17.png) 简单总结下:如果你要把一个庞大到撑满你硬盘的远程仓库拉取到本地,而你只需要其中的一部分目录和文件,那就可以试一试 `git sparse-checkout` 了。 @@ -1147,7 +1147,7 @@ GitHub 仓库可以免费用,空间也无限大,但云服务可是要抠抠 不得不说,Git 实在是太强大了。就一行命令,解决了困扰我一天的烦恼,我的 80G 存储空间的云服务器又可以再战 3 年了,从此以后再也不用担心了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-18.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/sparse-checkout-18.png) Git 是真的牛逼,Linus 是真的牛逼,神不愧是神! @@ -1161,16 +1161,9 @@ Git 是真的牛逼,Linus 是真的牛逼,神不愧是神! 参考资料: ->- 维基百科:[https://zh.wikipedia.org/wiki/Git](https://zh.wikipedia.org/wiki/Git) +>- 维基百科:https://zh.wikipedia.org/wiki/Git >- hutusi:[改变世界的一次代码提交](https://mp.weixin.qq.com/s/gM__sQPILkAKWsMejOO8cA) ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) + diff --git a/docs/git/progit.md b/docs/git/progit.md new file mode 100644 index 0000000000..a7ac49e746 --- /dev/null +++ b/docs/git/progit.md @@ -0,0 +1,38 @@ +今天给大家分享一本个人最近看过觉得非常不错的Git开源手册,可能有些小伙伴也看过了,我是最近在通勤路上用PAD看的。这本开源手册,它除了有**PDF版**,还有**epub电子书版**,非常适合电子阅读,有需要的小伙伴可以在文末自行下载: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-01.png) + +相信看完对于个人Git知识体系的梳理和掌握是非常有帮助的。 + +这本手册在豆瓣上评价极高,之前9.3,现在也有9.1的高分,其作者是GitHub的员工,内容主要侧重于各种场合中的惯用法和底层原理的讲述,手册中还针对不同的使用场景,设计了几个合适的版本管理策略。简而言之,这本手册无论是对于初学者还是想进一步了解Git工作原理的开发者都非常合适。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-02.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-03.png) + +这个手册一共分为十章,详细内容如下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-04.png) + +**手册中部分内容展示如下:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-05.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-06.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-07.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-08.png) + +**需要该Git手册PDF+epub电子书的小伙伴:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/git/progit-09.png) + +可直接长按扫码关注下方二维码,回复 「**git**」 即可下载: + +![(长按扫码识别)](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/itwanger.png) + + +好了,这次资源分享就到这里!后续如果遇到有用的工具或者资源,依然还会持续分享,也欢迎大家多多安利和交流,一起分享成长。 + +以上,我们下篇见。 diff --git a/docs/gongju/Chocolatey-Homebrew.md b/docs/gongju/Chocolatey-Homebrew.md new file mode 100644 index 0000000000..6e94190936 --- /dev/null +++ b/docs/gongju/Chocolatey-Homebrew.md @@ -0,0 +1,231 @@ +--- +category: + - Java企业级开发 +tag: + - 辅助工具/轮子 +title: Chocolatey Homebrew:两款惊艳的Shell软件管理器 +--- + +小二是公司新来的实习生,之前面试的过程中对答如流,所以我非常看好他。第一天,我给他了一台新电脑,要他先在本地搭建个 Java 开发环境。 + +二话不说,他就开始马不停蹄地行动了。真没想到,他竟然是通过命令行的方式安装的 JDK,这远远超出了我对他的预期。 + +我以为,他会使用图形化的方式来安装 JDK 的,就像这样。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-3d58d8db-e851-4c3d-97a5-66c8bf94d420.png) + +还有这样。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-eda6f2a0-a192-4b92-b3cc-46f679ec5bcb.png) + +结果他是这样的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-371831e9-4580-4097-a0bf-d9c460f493fb.png) + +卧槽!牛逼高大上啊! + +看着他熟练地在命令行里安装 JDK 的样子,我的嘴角开始微微上扬,真不错!这次总算招到了一个靠谱的。 + +于是我就安排他做一个记录,打算发表在我的小破站《Java 程序员进阶之路》上。从他嘴里了解到,他用的命令行软件管理器叫 chocolatey,这是一个Windows下的命令行软件管理器,可以方便开发者像在Linux下使用yum命令来安装软件,或者像在macOS下使用brew 命令来安装软件,我感觉非常酷炫。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-509ea602-39b0-462e-9b8d-2a55fc31a4c8.png) + + +以下是他的记录,一起来欣赏下。 + +### 关于shell + +对于一名 Java 后端程序员来说,初学阶段,你可以选择在 IDE 中直接编译运行 Java 代码,但有时候也需要在 Shell 下编译和运行 Java 代码。 + +>Windows 下自带的 Shell 叫命令提示符,或者 cmd 或者 powershell,macOS 下叫终端 terminal。 + +- [终端与 Shell 的区别](https://mp.weixin.qq.com/s?__biz=MzIxNzQwNjM3NA==&mid=2247491253&idx=1&sn=9a46879174f7240267fe5b5205d16d22&scene=21#wechat_redirect) +- [初次体验 macOS 下的 Shell](https://mp.weixin.qq.com/s/oEo8N3nE0wR1zl7qD4nh3w) + +但当你需要在生产环境下部署 Java项目或者查看日志的话,就必然会用到 Shell,这个阶段,Shell 的使用频率高到可以用一个成语来形容——朝夕相伴。 + +一些第三方软件会在原生的 Shell 基础上提供更强大的功能,常见的有 tabby、Warp、xhsell、FinalShell、MobaXterm、Aechoterm、WindTerm、termius、iterm2 等等,有些只能在 Windows 上使用,有些只能在 macOS 上使用,有些支持全平台。还有 ohmyzsh 这种超神的 Shell 美化工具。 + +这里,我们列举一些 Shell 的基本操作命令(Windows 和 macOS/Linux 有些许差异): + +- 切换目录,可以使用 cd 命令切换目录,`cd ..` 返回上级目录。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-d4f0634f-7b5b-4bbd-97eb-74ec1fe059c5.png) + +- 目录列表,macos/linux 下可以使用 ls 命令列出目录下所有的文件和子目录(Windows 下使用 dir 命令),使用通配符 `*` 对展示的内容进行过滤,比如 `ls *.java` 列出所有 `.java`后缀的文件,如果想更进一步的话,可以使用 `ls H*.java` 列出所有以 H 开头 `.java` 后缀的文件。 +- 新建目录,macOS/Linux 下可以使用 mkdir 命令新建一个目录(比如 `mkdir hello` 可以新建一个 hello 的目录),Windows 下可以使用 md 命令。 +- 删除文件,macOS/Linux 下可以使用 `rm` 命令删除文件(比如 `rm hello.java` 删除 hello.java 文件),Windows 下可以使用 del 命令。 +- 删除目录,macOS/Linux 下可以使用 `rm -r` 命令删除目录以及它所包含的所有文件(比如说 `rm -r hello` 删除 hello 目录)。Windows 下可以使用 deltree 命令。 +- 重复命令,macOS/Linux/Windows 下都可以使用上下箭头来选择以往执行过的命令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-738501d4-c210-449c-9d6c-56d5d16c623b.png) + +- 命令历史,macOS/Linux 下可以使用 `history` 命令查看所有使用过的命令。Windows 可以按下 F7 键。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-e90c8f1f-c46c-47da-be88-0e8a00d85089.png) + +- 解压文件,后缀名为“.zip”的文件是一个包含了其他文件的压缩包,macOS/Linux 系统自身已经提供了用于解压的 unzip 命令, Windows 的话需要手动安装。 + +### 安装JDK + +**1)Windows** + +推荐先安装 chocolatey。这是一个Windows下的命令行软件管理器,可以方便开发者像在Linux下使用yum命令来安装软件,或者像在macOS下使用brew 命令来安装软件,非常酷炫。 + +>The biggest challenge is reducing duplication of effort, so users turn to Chocolatey for simplicity + +传统的安装方式要么非常耗时,要么非常低效,在命令行安装软件除了简单高效,还能自动帮我们配置环境变量。 + +>- 官方地址:[https://chocolatey.org/](https://chocolatey.org/) +>- 安装文档:[https://chocolatey.org/install#individual](https://chocolatey.org/install#individual) + +安装完成后如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-5fd2b098-5685-49be-b424-f13ac288858d.png) + +如果不确定是否安装成功的话,可以通过键入 `choco` 命令来确认。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-a7ea03ef-aa60-47a8-adfc-8b6dde1cf4cc.png) + +这里推荐几个非常高效的操作命令: + +- choco search xxx,查找 xxx 安装包 +- choco info xxx,查看 xxx 安装包信息 +- choco install xxx,安装 xxx 软件 +- choco upgrade xxx,升级 xxx 软件 +- choco uninstall xxx, 卸载 xxx 软件 + +如何知道 chocolatey 仓库中都有哪些安装包可用呢? + +可以通过上面提到的命令行的方式,也可以访问官方仓库进行筛选。 + +>[https://community.chocolatey.org/packages](https://community.chocolatey.org/packages) + +比如说我们来查找 Java。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-a5d6ff0f-f36d-4eb0-a69e-2ba30dd315ea.png) + +好,现在可以直接在shell中键入 `choco install jdk8` 来安装 JDK8 了,并且会自动将Java加入到环境变量中,不用再去「我的电脑」「环境变量」中新建 JAVA_HOME 并复制 JDK 安装路径配置 PATH 变量了,是不是非常 nice? + +稍等片刻,键入 `java -version` 就可以确认Java是否安装成功了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-31441e14-b692-4abc-8640-a8b70e08dfae.png) + +不得不承认!非常nice! + +**2)macOS** + +首先推荐安装 homebrew,这是macOS下的命令行软件管理器,用来简化 macOS 上软件的安装过程。homebrew 是开源的,在 GitHub 已收获 32k star。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-0f9c86a0-e4b2-4103-a77e-8771ab8253f5.png) + +homebrew 的安装也非常的简单,只需要一行命令即可。 + +>官方网址:[https://brew.sh/index_zh-cn](https://brew.sh/index_zh-cn) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-b1e8ea1c-2aec-4f79-bdc8-eb3e4fe1a0f0.png) + +- 使用 `brew install xxx` 可以安装 macOS 上没有预装的软件 +- 使用 `brew install --cask yyy` 可以安装 macOS 其他非开源软件。 + +这里是 homebrew 常用命令的一个清单,可供参考。 + +命令| 描述 +---|--- +brew update| 更新 Homebrew +brew search package| 搜索软件包 +brew install package| 安装软件包 +brew uninstall package| 卸载软件包 +brew upgrade| 升级所有软件包 +brew upgrade package| 升级指定软件包 +brew list| 列出已安装的软件包列表 +brew services command package| 管理 brew 安装软件包 +brew services list| 列出 brew 管理运行的服务 +brew info package| 查看软件包信息 +brew deps package| 列出软件包的依赖关系 +brew help| 查看帮助 +brew cleanup| 清除过时软件包 +brew link package| 创建软件包符号链接 +brew unlink package| 取消软件包符号链接 +brew doctor| 检查系统是否存在问题 + +安装完 homebrew 后,建议替换homebrew 的默认源为中科大的,原因就不用我多说了吧?替换方法如下所示: + +``` +替换brew.git: +cd "$(brew --repo)" +git remote set-url origin https://mirrors.ustc.edu.cn/brew.git + +替换homebrew-core.git: +cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core" +git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git +``` + +如何知道 homebrew 仓库中都有哪些安装包可用呢? + +第一种,通过 `brew search xxx` 命令搜索,比如说我们要搜索 jdk + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-11bdc7b9-5060-4563-9e14-1bc9013882ce.png) + +第二种,通过 homebrew 官网搜索,比如说我们要搜索 openjdk。 + + +>官方地址:[https://formulae.brew.sh](https://formulae.brew.sh) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-80b4318e-0491-4ebd-8047-7e4528426921.png) + +这里有一份不错的 homebrew 帮助文档,可供参考: + + +>[https://sspai.com/post/56009](https://sspai.com/post/56009) + + + +OK,我们来安装JDK,只需要简单的一行命令就可以搞定。 + +`brew install openjdk@8` + +对比下载安装包,通过图形化界面的方式安装 JDK,是不是感觉在 Shell 下安装 JDK 更炫酷一些? + +关键是还省去了环境变量的配置。 + +记得还没有走出新手村的时候,就经常被环境变量配置烦不胜烦。那下载这种命令行的方式,要比手动在环境变量中配置要省事一百倍,也更不容易出错。 + +### 关于编辑器 + +安装完 Java 之后,你还需要一个编辑器,用来编写 Java 代码。 + +编辑器多种多样,常见的有集成开发环境(IDE,比如 Intellij IDEA 和 vscode),和简单的文本编辑工具(比如 sublime text)。 + +我建议这三个工具都要装,日常开发中,我会在这三个编辑器中来回切换。 + +Intellij IDEA:主要用来编写Java代码,并且最好安装旗舰版,社区版用来学习JavaSE部分是绰绰有余的,但要想拥有更强大的生产力,旗舰版是必须的,因为功能更加强大。 + +比如说 idea 旗舰版中可以直接通过 Initializr 来创建springboot项目,但社区版就没有此功能。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-d493229a-d62e-42cc-a6f0-7e3c0893a0c5.png) + + +vscode:更加轻量级的 IDE,在编写Java代码上可以和idea媲美,但要想调试Java代码的话,vscode 和idea的差距还是非常明显的。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-5e913932-58a8-415a-a1aa-30a7e0f2b8aa.png) + +我会使用 Intellij IDEA 开发编程喵的后端代码,vscode 来开发编程喵的前端代码。 + + +sublime text:功能更强大的文本编辑器,比记事本这种强大一万倍,也更符合21世纪开发者的外观审美。如果只是简单的修改一下代码格式,或者注释,显然更加方便,因为idea还是比较吃内存的,出差旅行的时候,在笔记本上紧急修改一些代码时,更易用。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/Chocolatey-Homebrew-85aaca81-9e18-4ba9-aaf2-2989abb85fc5.png) + +我会配合 GitHub 桌面版来使用 sublime text,编辑 MD 文档的时候会比较舒服。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/gongju/DBeaver.md b/docs/gongju/DBeaver.md new file mode 100644 index 0000000000..cf4193ff57 --- /dev/null +++ b/docs/gongju/DBeaver.md @@ -0,0 +1,165 @@ +--- +title: DBeaver:一款免费的数据库操作工具 +category: + - Java企业级开发 +tag: + - 辅助工具/轮子 +--- + + +作为一名开发者,免不了要和数据库打交道,于是我们就需要一款顺手的数据库管理工具。很长一段时间里,Navicat 都是我的首选,但最近更换了一台新电脑,之前的绿色安装包找不到了。 + +于是就琢磨着,找一款免费的,功能和 Navicat 有一拼的数据库管理工具来替代。好朋友 macrozheng 给我推荐了 DBeaver,试用完后体验真心不错,于是就来给大家安利一波。 + +### 一、关于 DBeaver + +DBeaver 是一个跨平台的数据库管理工具,支持 Windows、Linux 和 macOS。它有两个版本,企业版和社区版,对于个人开发者来说,社区版的功能已经足够强大。 + +DBeaver 是由 Java 编写的,默认使用 JDK 11 进行编译。社区版基于 [Apache-2.0 License](https://github.com/dbeaver/dbeaver/blob/devel/LICENSE.md) 在 GitHub 上开源,目前已获得 24k+ 的星标。 + +>[https://github.com/dbeaver/dbeaver](https://github.com/dbeaver/dbeaver) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-1.png) + +DBeaver 支持几乎所有主流的数据库,包括关系型数据库和非关系数据库。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-2.png) + +### 二、安装 DBeaver + +可以通过 DBeaver 官方下载安装包,也可以通过 GitHub 下载 release 版本。 + +>官方下载地址:[https://dbeaver.io/download/](https://dbeaver.io/download/) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-3.png) + +根据自己电脑的操作系统下载对应的安装包,完整安装后,第一步要做的是配置 Maven 镜像,否则在后续下载数据库驱动的时候会非常的慢。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-4.png) + + +因为 DBeaver 是基于 [Maven 构建](https://github.com/itwanger/toBeBetterJavaer/blob/master/docs/maven/maven.md)的,数据库驱动也就是链接数据库的 JDBC 驱动是通过 Maven 仓库下载的。选择「首选项」→「Maven」,添加阿里云镜像地址: + +>[http://maven.aliyun.com/nexus/content/groups/public](http://maven.aliyun.com/nexus/content/groups/public) + +和配置 Maven 镜像一样,如下图所示。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-5.png) + +配置完成后,记得把阿里云镜像仓库置顶。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-6.png) + + +### 三、管理数据源 + +像使用 Navicat 一样,我们需要先建立连接,这里就以 MySQL 为例。点击「连接」小图标,选择数据库。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-7.png) + +点击下一步,这时候需要填写数据库连接信息。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-8.png) + +点击「测试链接」,如果使用默认的 Maven 仓库时,下载驱动会非常慢,如下图所示,还容易失败「踩过的坑就不要再踩了」。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-9.png) + +如果你前面按照我说的配置了阿里云的 Maven 镜像,程序就不一样了,点了「测试链接」,瞬间会弹出「连接已成功」的提示框。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-10.png) + +链接成功后,就可以看到数据库中的表啊、视图啊、索引啊等等。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-11.png) + +### 四、管理表 + +数据库连接成功后,最重要的还是操作表。 + +**01、查看表** + +选择一张表,双击后就可以看到表的属性了,可以查看表的列、约束(主键)、外键、索引等等信息。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-12.png) + +点击「DDL(Data Definition Language,数据定义语言)」可以看到详细的建表语句。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-13.png) + +点击「数据」可以查看表的数据,底部有「新增」、「修改」、「删除」等行操作按钮。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-14.png) + +可以在顶部的过滤框中填写筛选条件,然后直接查询结果。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-15.png) + +如果不想显示某一列的话,可以直接点击「自定义结果集」图表,将某个字段的状态设置为不可见即可。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-16.png) + +**02、新增表** + +在左侧选择「表」,然后右键选择「新建表」即可建表id。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-17.png) + +之后在右侧列的区域右键,选择「新建列」即可添加字段。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-18.png) + +比如说我们新建一个主键 ID,如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-19.png) + +在 DBeaver 中,`[v]` 表示真,`[]` 表示否。紧接着在「约束」里选择 ID 将其设置为主键。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-20.png) + +最后点击保存,会弹出一个建表语句的预览框,点击「执行」即可完成表的创建。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-21.png) + +### 五、执行 SQL + +右键数据库表,选择右键菜单中的「SQL 编辑器」可以打开 SQL 编辑面板。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-22.png) + +然后编辑 SQL 语句,点击运行的小图标就可以查询数据了。这个过程会有语法提示,非常 nice。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-23.png) + +DBeaver 有一个很亮眼的操作就是,可以直接选中一条结果集,然后右键生成 SQL。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-24.png) + +比如说 insert 语句,这样再插入一条重复性内容的时候就非常方便了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-25.png) + +### 六、外观配置 + +可以在首选项里对外观进行设置,比如说把主题修改为暗黑色。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-26.png) + +然后界面就变成了暗黑系。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-27.png) + +还可以设置字体大小等。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-28.png) + +从整体的风格来看,DBeaver 和 Eclipse 有些类似,事实上也的确如此,DBeaver 是基于 Eclipse 平台构建的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/DBeaver-29.png) + +### 七、总结 + +总体来说,DBeaver是一款非常优秀的开源数据库管理工具了,功能很全面,日常的开发基本上是够用了。对比收费的 Navicat 和 DataGrip,可以说非常良心了。大家如果遇到收费版不能使用的时候,可以来体验一下社区版 DBeaver。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/gongju/chiner.md b/docs/gongju/chiner.md new file mode 100644 index 0000000000..4b20320ef7 --- /dev/null +++ b/docs/gongju/chiner.md @@ -0,0 +1,162 @@ +--- +title: chiner:一款开源的数据库设计神器 +category: + - Java企业级开发 +tag: + - 辅助工具/轮子 + +--- + +最近在造轮子,从 0 到 1 的那种,就差前台的界面了,大家可以耐心耐心耐心期待一下。其中需要设计一些数据库表,可以通过 Navicat 这种图形化管理工具直接开搞,也可以通过一些数据库设计工具来搞,比如说 PowerDesigner,更专业一点。 + + 今天我给大家推荐的这款国人开源的数据库设计工具 chiner,界面漂亮,功能强大,体验后给我的感觉是真香...... + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-1.png) + + +### 一、关于 PowerDesigner + +PowerDesigner 是一款功能非常强大的建模工具,可以和 Rational Rose 媲美。Rose 专攻 UML 对象模型的建模,之后才拓展到数据库这块。而 PowerDesigner 是一开始就为数据库建模服务的,后来才发展为一款综合战斗力都还不错的建模工具。 + +不过,说句实在话,PowerDesigner 的界面偏古典一些,下面是我用 PowerDesigner 设计 DB 的效果。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-2.png) + +### 二、关于 chiner + +chiner,发音:[kaɪˈnər],使用React+Electron+Java技术体系构建的一款元数建模平台。 + +2018 年,作者和几个对开源有兴趣的社区好友开始打磨产品的原因,历经三代,直到 2021 年 7 月份,终于推出了船新的 3.0 版本。 + +2019 年底,团队差点解散,幸好有几位好友关照,给了团队两个项目做,这才算是熬了过去。 + +不得不说,做任何一件事情都不容易啊,光靠情怀也许可以撑过产品初期,但越往后去,遇到生存问题时,就会非常困难。 + +在此,我们必须得为每一位开源作者奉上最真诚的掌声,希望他们的产品都能有一番天地。也希望,未来我的产品出现在大家的面前时,能给它多一点点包容和支持。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-3.gif) + +### 三、安装 chiner + +chiner 支持 Windows、macOS 和 Linux,下载地址如下所示: + +>[https://gitee.com/robergroup/chiner/releases](https://gitee.com/robergroup/chiner/releases) + +码云做了外部链接的拦截,导致直接复制链接到地址栏才能完成下载。我这里以 macOS 为例。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-4.png) + +安装完成后首次打开的样子是这样的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-5.png) + +chiner 提供了非常贴心的操作手册和参考模板,如果时间比较充分的话,可以先把操作手册过一遍,写得非常详细。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-6.png) + +### 四、上手 chiner + +**01、导入导出** + +因为我之前有一份 PowerDesigner 文件,所以可以直接导入到 chiner。 + +第一步,新建一个项目 codingmore。 + +第二步,选择导入 PowerDesigner 文件。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-7.png) + +第三步,选择要添加的数据表。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-8.png) + +第四步,导入完成后,就可以点开单表进行查看了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-9.png) + +第五步,当完成重新设计后,就可以选择导出 DDL 到数据库表了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-10.png) + +当然了,也可以直接配置数据库 DB,这样就可以直接连接导入导出了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-11.png) + +导出的 SQL 文件可以直接通过宝塔面板上传到服务器端,然后再直接导入到数据库。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-12.png) + +如果需要用到数据库说明文档的话,也可以直接通过导出到 Word 文档来完成。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-13.png) + +**02、维护数据类型** + +chiner 自带了几种常见的数据类型,比如字串、小数、日期等,我们也可以根据自己的需要添加新的数据类型。 + +比如说默认的字串类型关联到其他数据库的类型如下所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-14.png) + +数据域是在数据类型的基础上,基于当前项目定义的有一定业务含义的数据类型,比如说我这里维护了一个长度为 90 的名称数据域。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-15.png) + +当我需要把某个数据字段的数据域设置成「名称」的时候,长度就会自动填充为 90,不需要手动再去设置。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-16.png) + +**03、维护数据表** + +第一步,选中数据表,右键选择「新增数据表」 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-17.png) + +第二步,填写数据表名 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-18.png) + +点击「确定」后,chiner 会帮我们自动生成一些常见常用的字段,比如说创建人、创建时间、更新人、更新时间等,非常的智能化。通常来说,这些字段都是必须的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-19.png) + +如果这些默认字段不满足需求的时候,还可以点击「设置」新增默认字段,比如说删除标记,一般来说为了安全起见,数据库都会采用非物理删除。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-20.png) + +一般来说,我们更习惯字段小写命名,因此可以直接选中一列,然后选择大小写转换。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-21.png) + +就变成小写了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-22.png) + +**04、维护关系图** + +第一步,选择「关系图」,右键选择「新增关系图」 + +第二步,把需要关联的表拖拽到右侧的面板当中,然后按照字段进行连线,非常的方便。比如说班级和学院表、班级和专业表的关系,就如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-23.png) + +来看一下整体给出来的关系图,还是非常清爽的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/chiner-24.png) + +### 五、尾声 + +chiner 还有更多更强大的功能,大家觉得不错的话,可以去尝试一下。用的熟练的话,肯定能在很大程度上提高生产效率。 + +就我个人的使用体验来说,chiner 比 PowerDesigner 更轻量级,也更符合日常的操作习惯,为国产开源点赞! + +项目地址: + +>[https://gitee.com/robergroup/chiner](https://gitee.com/robergroup/chiner) + +使用手册: + +>[https://www.yuque.com/chiner/docs/manual](https://www.yuque.com/chiner/docs/manual) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/gongju/choco.md b/docs/gongju/choco.md new file mode 100644 index 0000000000..2e385b89a2 --- /dev/null +++ b/docs/gongju/choco.md @@ -0,0 +1,179 @@ +--- +category: + - Java企业级开发 +tag: + - 辅助工具/轮子 +title: chocolatey:一款 GitHub 星标 8.2k+的Windows命令行软件管理器 + +--- + +小二是公司新来的实习生,之前面试的过程中对答如流,所以我非常看好他。第一天,我给他了一台新电脑,要他先在本地搭建个 Java 开发环境。 + +二话不说,他就开始马不停蹄地行动了。**真没想到,他竟然是通过命令行的方式安装的 JDK,一行命令就搞定了!连环境变量都不用配置,这远远超出了我对他的预期**。 + +我以为,他会傻乎乎地下一步下一步来安装 JDK,就像这样。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-9050933d-61f2-4d79-bad7-982803fc174a) + +然后这样配置环境变量。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-4adb249c-a662-42af-9c7e-08deeb11c835) + +结果他是这样的,就一行命令,环境变量也不用配置! + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-5c996f2a-6d14-4f9d-acd4-ff1cf959b55f) + +卧槽!牛逼高大上啊! + +看着他熟练地在命令行里安装 JDK 的样子,我的嘴角开始微微上扬,真不错!这次总算招到了一个靠谱的。 + +于是我就安排他做一个记录,打算发表在我的小破站《Java 程序员进阶之路》上。从他嘴里了解到,他用的命令行软件管理器叫 chocolatey,这是一个Windows下的命令行软件管理器,在 GitHub 上已经收获 8.2k+的星标,可以方便开发者像在Linux下使用yum命令来安装软件,或者像在macOS下使用brew 命令来安装软件,非常酷炫。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-c598af06-7a85-47ca-ade4-e07c60463990) + + +以下是他的记录,一起来欣赏下。 + +### 先来了解 shell + +对于一名 Java 后端程序员来说,初学阶段,你可以选择在 IDE 中直接编译运行 Java 代码,但有时候也需要在 Shell 下编译和运行 Java 代码。 + +>Windows 下自带的 Shell 叫命令提示符,或者 cmd 或者 powershell,macOS 下叫终端 terminal。 + +但当你需要在生产环境下部署 Java项目或者查看日志的话,就必然会用到 Shell,这个阶段,Shell 的使用频率高到可以用一个成语来形容——朝夕相伴。 + +一些第三方软件会在原生的 Shell 基础上提供更强大的功能,常见的有 tabby、Warp、xhsell、FinalShell、MobaXterm、Aechoterm、WindTerm、termius、iterm2 等等,有些只能在 Windows 上使用,有些只能在 macOS 上使用,有些支持全平台。还有 ohmyzsh 这种超神的 Shell 美化工具。 + +这里,我们列举一些 Shell 的基本操作命令(Windows 和 macOS/Linux 有些许差异): + +- 切换目录,可以使用 cd 命令切换目录,`cd ..` 返回上级目录。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-b329cba9-4982-44ec-9935-3c9eb5b92eab) + +- 目录列表,macos/linux 下可以使用 ls 命令列出目录下所有的文件和子目录(Windows 下使用 dir 命令),使用通配符 `*` 对展示的内容进行过滤,比如 `ls *.java` 列出所有 `.java`后缀的文件,如果想更进一步的话,可以使用 `ls H*.java` 列出所有以 H 开头 `.java` 后缀的文件。 +- 新建目录,macOS/Linux 下可以使用 mkdir 命令新建一个目录(比如 `mkdir hello` 可以新建一个 hello 的目录),Windows 下可以使用 md 命令。 +- 删除文件,macOS/Linux 下可以使用 `rm` 命令删除文件(比如 `rm hello.java` 删除 hello.java 文件),Windows 下可以使用 del 命令。 +- 删除目录,macOS/Linux 下可以使用 `rm -r` 命令删除目录以及它所包含的所有文件(比如说 `rm -r hello` 删除 hello 目录)。Windows 下可以使用 deltree 命令。 +- 重复命令,macOS/Linux/Windows 下都可以使用上下箭头来选择以往执行过的命令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-bdd0a107-512d-4906-9555-38fe06d24d5a) + +- 命令历史,macOS/Linux 下可以使用 `history` 命令查看所有使用过的命令。Windows 可以按下 F7 键。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-d6425fe6-1501-412e-9642-990864e6f810) + +- 解压文件,后缀名为“.zip”的文件是一个包含了其他文件的压缩包,macOS/Linux 系统自身已经提供了用于解压的 unzip 命令, Windows 的话需要手动安装。 + +### 再来了解chocolatey + +先安装 chocolatey。这是一个Windows下的命令行软件管理器,可以方便开发者像在Linux下使用yum命令来安装软件,或者像在macOS下使用brew 命令来安装软件,非常酷炫。 + +>The biggest challenge is reducing duplication of effort, so users turn to Chocolatey for simplicity + +传统的安装方式要么非常耗时,要么非常低效,在命令行安装软件除了简单高效,还能自动帮我们配置环境变量。 + +>- 官方地址:[https://chocolatey.org/](https://chocolatey.org/) +>- 安装文档:[https://chocolatey.org/install#individual](https://chocolatey.org/install#individual) + +第一步,以管理员的身份打开 cmd 命令行。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-f01fb23b-9ca5-4997-acc3-26ebd7da060f) + +第二步,执行以下命令: + +``` +Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) +``` + +稍等片刻,就完成安装了。 + +安装完成后如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-e9696a71-ceb5-4c62-b39f-876f742b6435) + +如果不确定是否安装成功的话,可以通过键入 `choco` 命令来确认。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-211a2335-d3e1-49b5-8735-feabacf8087e) + +这里推荐几个非常高效的操作命令: + +- choco search xxx,查找 xxx 安装包 +- choco info xxx,查看 xxx 安装包信息 +- choco install xxx,安装 xxx 软件 +- choco upgrade xxx,升级 xxx 软件 +- choco uninstall xxx, 卸载 xxx 软件 + +如何知道 chocolatey 仓库中都有哪些安装包可用呢? + +可以通过上面提到的命令行的方式,也可以访问官方仓库进行筛选。 + +>[https://community.chocolatey.org/packages](https://community.chocolatey.org/packages) + +比如说我们来查找 Java。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-a4bdc885-22a9-4307-b4b6-cd1ad1f0ba8b) + +好,现在可以直接在shell中键入 `choco install jdk8` 来安装 JDK8 了,并且会自动将Java加入到环境变量中,不用再去「我的电脑」「环境变量」中新建 JAVA_HOME 并复制 JDK 安装路径配置 PATH 变量了,是不是非常 nice? + +稍等片刻,键入 `java -version` 就可以确认Java是否安装成功了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-80a6ced8-c25d-4371-8096-b95be48af768) + +不得不承认!非常nice! + +再比如说安装 Redis,只需要找到 Redis 的安装命令在 Choco 下执行一下就 OK 了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-488b71e7-8082-45b7-b454-3187938bf101) + +安装 Git: + +``` +choco install git.install +``` + +安装 node.js + +``` +choco install nodejs.install +``` + +安装 7zip + +``` +choco install 7zip +``` + +安装 **Filezilla** + +``` +choco install filezilla +``` + +Choco 上的软件包也非常的多,基本上软件开发中常见的安装包都有。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/choco-3a59dfe8-6c2d-4c5b-9187-bf2812a59041) + + +### 小结 + +通过小二的实战笔记,我们可以了解到。 + +对比下载安装包,通过图形化界面的方式安装 JDK,然后下一步,下一步是不是感觉在 Shell 下安装 JDK 更炫酷一些? + +关键是还省去了环境变量的配置。 + +记得还没有走出新手村的时候,就经常被环境变量配置烦不胜烦。那下载这种命令行的方式,要比手动在环境变量中配置要省事一百倍,也更不容易出错。 + +通过 Choco 可以集中安装、管理、更新各种各样的软件。特别适合管理一些轻量级的开源软件,一条命令搞定,升级的时候也方便,不用再重新去下载新的安装包,可以有效治愈更新强迫症患者的症状。 + +如果不想特殊设置的话,Chocolatey 整体的操作与使用还是比较亲民的。就连刚接触软件开发的小白也可以直接使用,而且路人看着会觉得你特别厉害。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + diff --git a/docs/src/gongju/fastjson.md b/docs/gongju/fastjson.md similarity index 93% rename from docs/src/gongju/fastjson.md rename to docs/gongju/fastjson.md index dcc69913bb..c5e36e795b 100644 --- a/docs/src/gongju/fastjson.md +++ b/docs/gongju/fastjson.md @@ -12,7 +12,7 @@ tag: 我是 fastjson,是个地地道道的杭州土著,但我始终怀揣着一颗走向全世界的雄心。这不,我在 GitHub 上的简介都换成了英文,国际范十足吧? -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-0576767f-c447-49f1-83a3-6971782c4d52.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/fastjson-0576767f-c447-49f1-83a3-6971782c4d52.png) 如果你的英语功底没有我家老板 666 的话,我可以简单地翻译下(说人话,不装逼)。 @@ -233,7 +233,7 @@ public class IdentityHashMap { 再比如说,使用 asm 技术来避免反射导致的开销。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-86a38cb0-3acc-4132-8e1f-48ebeaa52b47.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/fastjson-86a38cb0-3acc-4132-8e1f-48ebeaa52b47.png) 我承认,快的同时,也带来了一些安全性的问题。尤其是 AutoType 的引入,让黑客有了可乘之机。 @@ -257,14 +257,14 @@ public class IdentityHashMap { 在于黑客的反复较量中,我虽然变得越来越稳重成熟了,但与此同时,让我的用户为此也付出了沉重的代价。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-6868c673-8799-4326-baab-1050a5a4e9a3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/fastjson-6868c673-8799-4326-baab-1050a5a4e9a3.png) 网络上也出现了很多不和谐的声音,他们声称我是最垃圾的国产开源软件之一,只不过凭借着一些投机取巧赢得了国内开发者的信赖。 但更多的是,对我的不离不弃。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-85a44233-6eb2-4164-a091-6b65fc5f001a.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/fastjson-85a44233-6eb2-4164-a091-6b65fc5f001a.png) 最令我感到为之动容的一句话是: @@ -274,7 +274,7 @@ public class IdentityHashMap { 为了彻底解决 AutoType 带来的问题,在 1.2.68 版本中,我引入了 safeMode 的安全模式,无论白名单和黑名单,都不支持 AutoType,这样就可以彻底地杜绝攻击。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-57146979-cb99-4236-94f9-1cd5276e8269.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/fastjson-57146979-cb99-4236-94f9-1cd5276e8269.png) 安全模式下,`checkAutoType()` 方法会直接抛出异常。 @@ -285,4 +285,4 @@ public class IdentityHashMap { 2020 年的最后一篇文章!看到的就点个赞吧,2021 年顺顺利利。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/forest.md b/docs/gongju/forest.md similarity index 97% rename from docs/src/gongju/forest.md rename to docs/gongju/forest.md index f282f7c0b8..762db306ae 100644 --- a/docs/src/gongju/forest.md +++ b/docs/gongju/forest.md @@ -141,11 +141,11 @@ Forest 的字面意思是森林的意思,更内涵点的话,可以拆成For **虽然 star 数还不是很多,但 star 趋势图正在趋于爬坡阶段,大家可以拿来作为一个练手项目,我觉得还是不错的选择**。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/forest-55b54f3f-88a7-458b-b8e0-b0d60e916d5e.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/forest-55b54f3f-88a7-458b-b8e0-b0d60e916d5e.png) Forest 本身是处理前端过程的框架,是对后端 HTTP API 框架的进一步封装。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/forest-41345eec-fe16-4fcf-9448-0cb8c57d515f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/forest-41345eec-fe16-4fcf-9448-0cb8c57d515f.png) 前端部分: @@ -382,4 +382,4 @@ myClient.send("foo", (String resText, ForestRequest request, ForestResponse resp 这篇文章不仅介绍了 Forest 这个轻量级的 HTTP 客户端框架,还回顾了它的底层实现:HttpClient 和 OkHttp,希望能对大家有所帮助。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/gson.md b/docs/gongju/gson.md similarity index 97% rename from docs/src/gongju/gson.md rename to docs/gongju/gson.md index ce70a00711..67653fb6f9 100644 --- a/docs/src/gongju/gson.md +++ b/docs/gongju/gson.md @@ -243,7 +243,7 @@ class Bar{ 假如你 debug 的时候,进入到 `toJson()` 方法的内部,就可以观察到。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/gson-402ff6b5-a460-45de-ab62-ede6fbf6b61e.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/gson-402ff6b5-a460-45de-ab62-ede6fbf6b61e.png) foo 的实际类型为 `Foo`,但我女朋友在调用 `foo.getClass()` 的时候,只会得到 Foo,这就意味着她并不知道 foo 的实际类型。 @@ -283,11 +283,11 @@ Bar bar1 = foo1.get(); debug 进入 `toJson()` 方法内部查看的话,就可以看到 foo 的真实类型了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/gson-1c6eac43-6f0b-4a00-ae6c-2db29f911719.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/gson-1c6eac43-6f0b-4a00-ae6c-2db29f911719.png) `fromJson()` 在反序列化的时候,和此类似。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/gson-5afe5cd1-4966-4b16-adcb-fc04edfff406.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/gson-5afe5cd1-4966-4b16-adcb-fc04edfff406.png) 这样的话,bar1 就可以通过 `foo1.get()` 到了。 @@ -455,4 +455,4 @@ private int age = 18; 如果你觉得我有点用的话,不妨点个赞,留个言,see you。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/jackson.md b/docs/gongju/jackson.md similarity index 98% rename from docs/src/gongju/jackson.md rename to docs/gongju/jackson.md index c2ab9fef63..5bde839e17 100644 --- a/docs/src/gongju/jackson.md +++ b/docs/gongju/jackson.md @@ -19,7 +19,7 @@ Java 之所以牛逼,很大的功劳在于它的生态非常完备,JDK 没 当我们通过 starter 新建一个 Spring Boot 的 Web 项目后,就可以在 Maven 的依赖项中看到 Jackson 的身影。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/jackson-4340975c-e254-4287-88e0-66f73fe88889.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/jackson-4340975c-e254-4287-88e0-66f73fe88889.png) Jackson 有很多优点: @@ -47,7 +47,7 @@ Jackson 的核心模块由三部分组成: jackson-databind 依赖于 jackson-core 和 jackson-annotations,所以添加完 jackson-databind 之后,Maven 会自动将 jackson-core 和 jackson-annotations 引入到项目当中。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/jackson-24990211-7a18-44d7-aff0-6ac9e3cf0561.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/jackson-24990211-7a18-44d7-aff0-6ac9e3cf0561.png) Maven 之所以讨人喜欢的一点就在这,能偷偷摸摸地帮我们把该做的做了。 @@ -563,4 +563,4 @@ Woman{age=18, name='三妹'} 好了,通过这篇文章的系统化介绍,相信你已经完全摸透 Jackson 了,我们下篇文章见。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/junit.md b/docs/gongju/junit.md similarity index 91% rename from docs/src/gongju/junit.md rename to docs/gongju/junit.md index d472e52fbb..daeb3dfac5 100644 --- a/docs/src/gongju/junit.md +++ b/docs/gongju/junit.md @@ -16,7 +16,7 @@ tag: 微软公司之前有这样一个统计:bug 在单元测试阶段被发现的平均耗时是 3.25 小时,如果遗漏到系统测试则需要 11.5 个小时。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-5b0afb32-c60e-4218-98b1-44288705e472.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-5b0afb32-c60e-4218-98b1-44288705e472.png) 经我这么一说,你应该已经很清楚单元测试的重要性了。那在你最初编写测试代码的时候,是不是经常这么做?就像下面这样。 @@ -56,17 +56,17 @@ public class Factorial { 第一步,直接在当前的代码编辑器窗口中按下 `Command+N` 键(Mac 版),在弹出的菜单中选择「Test...」。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-fe29e8b8-9264-4aa3-9139-6ebb39af88a1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-fe29e8b8-9264-4aa3-9139-6ebb39af88a1.png) 勾选上要编写测试用例的方法 `fact()`,然后点击「OK」。 此时,IDEA 会自动在当前类所在的包下生成一个类名带 Test(惯例)的测试类。如下图所示。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-756305d6-7166-4737-8665-89d24a1eefae.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-756305d6-7166-4737-8665-89d24a1eefae.png) 如果你是第一次使用我的话,IDEA 会提示你导入我的依赖包。建议你选择最新的 JUnit 5.4。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-91bf986d-3586-4175-9ca2-959e5eb62e9c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-91bf986d-3586-4175-9ca2-959e5eb62e9c.png) 导入完毕后,你可以打开 pom.xml 文件确认一下,里面多了对我的依赖。 @@ -95,11 +95,11 @@ void fact() { 第三步,你可以在邮件菜单中选择「Run FactorialTest」来运行测试用例,结果如下所示。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-7daf1a3d-a321-4d42-9d16-134043161a29.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-7daf1a3d-a321-4d42-9d16-134043161a29.png) 测试失败了,因为第 20 行的预期结果和实际不符,预期是 100,实际是 120。此时,你要么修正实现代码,要么修正测试代码,直到测试通过为止。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-5b71c36f-684d-4d30-b1a1-faef453603ae.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-5b71c36f-684d-4d30-b1a1-faef453603ae.png) 不难吧?单元测试可以确保单个方法按照正确的预期运行,如果你修改了某个方法的代码,只需确保其对应的单元测试通过,即可认为改动是没有问题的。 @@ -124,7 +124,7 @@ public class Calculator { 新建测试用例的时候记得勾选`setUp` 和 `tearDown`。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-afa3c969-a2d2-439c-b440-5b7480592d52.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/junit-afa3c969-a2d2-439c-b440-5b7480592d52.png) 生成后的代码如下所示。 @@ -307,4 +307,4 @@ void fromJava9to11() { 希望我能尽早的替你发现代码中的 bug,毕竟越早的发现,造成的损失就会越小。see you! -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/knife4j.md b/docs/gongju/knife4j.md similarity index 76% rename from docs/src/gongju/knife4j.md rename to docs/gongju/knife4j.md index 2ffa9d1599..58893b4562 100644 --- a/docs/src/gongju/knife4j.md +++ b/docs/gongju/knife4j.md @@ -7,27 +7,27 @@ tag: --- -一般在使用 Spring Boot 开发前后端分离项目的时候,都会用到 [Swagger](https://javabetter.cn/springboot/swagger.html)(戳链接详细了解)。 +一般在使用 Spring Boot 开发前后端分离项目的时候,都会用到 [Swagger](https://tobebetterjavaer.com/springboot/swagger.html)(戳链接详细了解)。 但随着系统功能的不断增加,接口数量的爆炸式增长,Swagger 的使用体验就会变得越来越差,比如请求参数为 JSON 的时候没办法格式化,返回结果没办法折叠,还有就是没有提供搜索功能。 今天我们介绍的主角 Knife4j 弥补了这些不足,赋予了 Swagger 更强的生命力和表现力。 -## 关于 Knife4j +### 关于 Knife4j Knife4j 的前身是 swagger-bootstrap-ui,是 springfox-swagger-ui 的增强 UI 实现。swagger-bootstrap-ui 采用的是前端 UI 混合后端 Java 代码的打包方式,在微服务的场景下显得非常臃肿,改良后的 Knife4j 更加小巧、轻量,并且功能更加强大。 springfox-swagger-ui 的界面长这个样子,说实话,确实略显丑陋。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-1.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-1.png) swagger-bootstrap-ui 增强后的样子长下面这样。单纯从直观体验上来看,确实增强了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-2.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-2.png) 那改良后的 Knife4j 不仅在界面上更加优雅、炫酷,功能上也更加强大:后端 Java 代码和前端 UI 模块分离了出来,在微服务场景下更加灵活;还提供了专注于 Swagger 的增强解决方案。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-3.png) 官方文档: @@ -41,7 +41,7 @@ swagger-bootstrap-ui 增强后的样子长下面这样。单纯从直观体验 >[https://gitee.com/xiaoym/swagger-bootstrap-ui-demo](https://gitee.com/xiaoym/swagger-bootstrap-ui-demo) -## 整合 Knife4j +### 整合 Knife4j Knife4j 完全遵循了 Swagger 的使用方式,所以可以无缝切换。 @@ -102,7 +102,7 @@ public class Knife4jController { } ``` -第四步,由于 springfox 3.0.x 版本 和 Spring Boot 2.6.x 版本有冲突,所以还需要先解决这个 bug,一共分两步(在[Swagger](https://javabetter.cn/springboot/swagger.html) 那篇已经解释过了,这里不再赘述,但防止有小伙伴在学习的时候再次跳坑,这里就重复一下步骤)。 +第四步,由于 springfox 3.0.x 版本 和 Spring Boot 2.6.x 版本有冲突,所以还需要先解决这个 bug,一共分两步(在[Swagger](https://tobebetterjavaer.com/springboot/swagger.html) 那篇已经解释过了,这里不再赘述,但防止有小伙伴在学习的时候再次跳坑,这里就重复一下步骤)。 先在 application.yml 文件中加入: @@ -157,17 +157,17 @@ public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { >访问地址(和 Swagger 不同):[http://localhost:8080/doc.html](http://localhost:8080/doc.html) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-0a9eb2b1-bace-4f47-ace9-8a5f9f280279.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-0a9eb2b1-bace-4f47-ace9-8a5f9f280279.png) 是不是比 Swagger 简洁大方多了?如果想测试接口的话,可以直接点击接口,然后点击「测试」,点击发送就可以看到返回结果了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-16b1b553-1667-4222-9f29-2e5dfc8917a0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-16b1b553-1667-4222-9f29-2e5dfc8917a0.png) -## Knife4j 的功能特点 +### Knife4j 的功能特点 编程喵🐱实战项目中已经整合好了 Knife4j,在本地跑起来后,就可以查看所有 API 接口了。编程喵中的管理端(codingmore-admin)端口为 9002,启动服务后,在浏览器中输入 [http://localhost:9002/doc.html](http://localhost:9002/doc.html) 就可以访问到了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-3cfbf598-b94a-4081-aab3-06af1eef612c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-3cfbf598-b94a-4081-aab3-06af1eef612c.png) 简单来介绍下 Knife4j 的 功能特点: @@ -176,68 +176,68 @@ public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { Knife4j 和 Swagger 一样,也是支持头部登录认证的,点击「authorize」菜单,添加登录后的信息即可保持登录认证的 token。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-6.png) 如果某个 API 需要登录认证的话,就会把之前填写的信息带过来。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-7.png) **2)支持 JSON 折叠** Swagger 是不支持 JSON 折叠的,当返回的信息非常多的时候,界面就会显得非常的臃肿。Knife4j 则不同,可以对返回的 JSON 节点进行折叠。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-8.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-8.png) **3)离线文档** Knife4j 支持把 API 文档导出为离线文档(支持 markdown 格式、HTML 格式、Word 格式), -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-9.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-9.png) 使用 Typora 打开后的样子如下,非常的大方美观。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-10.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-10.png) **4)全局参数** 当某些请求需要全局参数时,这个功能就很实用了,Knife4j 支持 header 和 query 两种方式。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-11.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-11.png) 之后进行请求的时候,就会把这个全局参数带过去。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-12.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-12.png) **5)搜索 API 接口** Swagger 是没有搜索功能的,当要测试的接口有很多的时候,当需要去找某一个 API 的时候就傻眼了,只能一个个去拖动滚动条去找。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-13.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-13.png) 在文档的右上角,Knife4j 提供了文档搜索功能,输入要查询的关键字,就可以检索筛选了,是不是很方便? -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-14.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-14.png) 目前支持搜索接口的地址、名称和描述。 -## 尾声 +### 尾声 除了我上面提到的增强功能,Knife4j 还提供了很多实用的功能,大家可以通过官网的介绍一一尝试下,生产效率会提高不少。 >[https://doc.xiaominfo.com/knife4j/documentation/enhance.html](https://doc.xiaominfo.com/knife4j/documentation/enhance.html) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-15.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/knife4j-15.png) ---- -更多内容,只针对《二哥的Java进阶之路》星球用户开放,需要的小伙伴可以[戳链接🔗](https://javabetter.cn/zhishixingqiu/)加入我们的星球,一起学习,一起卷。。**编程喵**🐱是一个 Spring Boot+Vue 的前后端分离项目,融合了市面上绝大多数流行的技术要点。通过学习实战项目,你可以将所学的知识通过实践进行检验、你可以拓宽自己的技术边界,你可以掌握一个真正的实战项目是如何从 0 到 1 的。 +更多内容,只针对《Java 程序员进阶之路》星球用户开放,需要的小伙伴可以[戳链接🔗](docs/zhishixingqiu/)加入我们的星球,一起学习,一起卷。。**编程喵**🐱是一个 Spring Boot+Vue 的前后端分离项目,融合了市面上绝大多数流行的技术要点。通过学习实战项目,你可以将所学的知识通过实践进行检验、你可以拓宽自己的技术边界,你可以掌握一个真正的实战项目是如何从 0 到 1 的。 ---- -## 源码路径 +### 源码路径 > - 编程喵:[https://github.com/itwanger/coding-more](https://github.com/itwanger/coding-more) > - codingmore-knife4j:[https://github.com/itwanger/codingmore-learning](https://github.com/itwanger/codingmore-learning/tree/main/codingmore-knife4j) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/log4j.md b/docs/gongju/log4j.md similarity index 94% rename from docs/src/gongju/log4j.md rename to docs/gongju/log4j.md index 308091e8b7..d7d046eb93 100644 --- a/docs/src/gongju/log4j.md +++ b/docs/gongju/log4j.md @@ -12,7 +12,7 @@ tag: 这不,我在战国时代读者群里发现了这么一串聊天记录: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-cacd9e88-4a4d-4127-a18b-f99b2e2296a3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-cacd9e88-4a4d-4127-a18b-f99b2e2296a3.png) 竟然有小伙伴不知道“打日志”是什么意思,不知道该怎么学习,还有小伙伴回答说,只知道 Log4j! @@ -22,7 +22,7 @@ tag: (说好的不在乎,怎么在乎起来了呢?手动狗头) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-58282c4d-8178-45bd-8ba3-26740f6dd4a3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-58282c4d-8178-45bd-8ba3-26740f6dd4a3.png) 管他呢,**我行我素**吧,保持初心不改就对了!这篇文章就来说说 Log4j,这个打印日志的鼻祖。Java 中的日志打印其实是个艺术活,我保证,这句话绝不是忽悠。 @@ -37,7 +37,7 @@ tag: 之所以这样打印日志,是因为很方便,上手难度很低,尤其是在 IDEA 的帮助下,只需在键盘上按下 `so` 两个字母就可以调出 `System.out.println()`。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-64dbb12b-8f6b-4ee3-ab5a-60519dd9112f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-64dbb12b-8f6b-4ee3-ab5a-60519dd9112f.png) 在本地环境下,使用 `System.out.println()` 打印日志是没问题的,可以在控制台看到信息。但如果是在生产环境下的话,`System.out.println()` 就变得毫无用处了。 @@ -63,7 +63,7 @@ OFF,最高级别,意味着所有消息都不会输出了。 这个级别是基于 Log4j 的,和 java.util.logging 有所不同,后者提供了更多的日志级别,比如说 SEVERE、FINER、FINEST。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-4919cd20-e524-43a2-8b41-9eab6ac0c1e4.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-4919cd20-e524-43a2-8b41-9eab6ac0c1e4.png) ### 03、错误的日志记录方式是如何影响性能的 @@ -86,7 +86,7 @@ if(logger.isDebugEnabled()){ 切记,在生产环境下,一定不要开启 DEBUG 级别的日志,否则程序在大量记录日志的时候会变很慢,还有可能在你不注意的情况下,悄悄地把磁盘空间撑爆。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-fd2149c5-2d0c-4c15-897d-d5fa06cce71f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-fd2149c5-2d0c-4c15-897d-d5fa06cce71f.png) ### 04、为什么选择 Log4j 而不是 java.util.logging @@ -123,7 +123,7 @@ public class JavaUtilLoggingDemo { 程序运行后会在 target 目录下生成一个名叫 javautillog.txt 的文件,内容如下所示: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-222181d4-04ba-4487-8386-69b8737d2d5c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-222181d4-04ba-4487-8386-69b8737d2d5c.png) 再来看一下 Log4j 的使用方式。 @@ -349,11 +349,11 @@ if(logger.isDebugEnabled()) { 8)不要在日志文件中打印密码、银行账号等敏感信息。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-42d3a052-daeb-450a-a775-a32f983dd688.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j-42d3a052-daeb-450a-a775-a32f983dd688.png) ### 06、 总结 打印日志真的是一种艺术活,搞不好会严重影响服务器的性能。最可怕的是,记录了日志,但最后发现屁用没有,那简直是苍了个天啊!尤其是在生产环境下,问题没有记录下来,但重现有一定的随机性,到那时候,真的是叫天天不应,叫地地不灵啊! -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/gongju/log4j2.md b/docs/gongju/log4j2.md new file mode 100644 index 0000000000..ac617061e7 --- /dev/null +++ b/docs/gongju/log4j2.md @@ -0,0 +1,312 @@ +--- +title: Log4j 2:Apache维护的一款高性能日志记录工具 +category: + - Java企业级开发 +tag: + - 辅助工具/轮子 +--- + +Log4j 2,顾名思义,它就是 Log4j 的升级版,就好像手机里面的 Pro 版。我作为一个写文章方面的工具人,或者叫打工人,怎么能不写完这最后一篇。 + +Log4j、SLF4J、Logback 是一个爹——Ceki Gulcu,但 Log4j 2 却是例外,它是 Apache 基金会的产品。 + +SLF4J 和 Logback 作为 Log4j 的替代品,在很多方面都做了必要的改进,那为什么还需要 Log4j 2 呢?我只能说 Apache 基金会的开发人员很闲,不,很拼,要不是他们这种精益求精的精神,这个编程的世界该有多枯燥,毕竟少了很多可以用“拿来就用”的轮子啊。 + +上一篇也说了,老板下死命令要我把日志系统切换到 Logback,我顺利交差了,老板很开心,夸我这个打工人很敬业。为了表达对老板的这份感谢,我决定偷偷摸摸地试水一下 Log4j 2,尽管它还不是个成品,可能会会项目带来一定的隐患。但谁让咱是一个敬岗爱业的打工人呢。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-a9461265-7652-4512-9219-6b3e82392415.png) + + +### 01、Log4j 2 强在哪 + +1)在多线程场景下,Log4j 2 的吞吐量比 Logback 高出了 10 倍,延迟降低了几个数量级。这话听起来像吹牛,反正是 Log4j 2 官方自己吹的。 + +Log4j 2 的异步 Logger 使用的是无锁数据结构,而 Logback 和 Log4j 的异步 Logger 使用的是 ArrayBlockingQueue。对于阻塞队列,多线程应用程序在尝试使日志事件入队时通常会遇到锁争用。 + +下图说明了多线程方案中无锁数据结构对吞吐量的影响。 Log4j 2 随着线程数量的扩展而更好地扩展:具有更多线程的应用程序可以记录更多的日志。其他日志记录库由于存在锁竞争的关系,在记录更多线程时,总吞吐量保持恒定或下降。这意味着使用其他日志记录库,每个单独的线程将能够减少日志记录。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-43f0b03d-5c4a-4af3-9e4c-177956246740.png) + +性能方面是 Log4j 2 的最大亮点,至于其他方面的一些优势,比如说下面这些,可以忽略不计,文字有多短就代表它有多不重要。 + +2)Log4j 2 可以减少垃圾收集器的压力。 + +3)支持 Lambda 表达式。 + +4)支持自动重载配置。 + +### 02、Log4j 2 使用示例 + +废话不多说,直接实操开干。理论知识有用,但不如上手实操一把,这也是我多年养成的一个“不那么良好”的编程习惯:在实操中发现问题,解决问题,寻找理论基础。 + +**第一步**,在 pom.xml 文件中添加 Log4j 2 的依赖: + +```xml + + org.apache.logging.log4j + log4j-api + 2.5 + + + org.apache.logging.log4j + log4j-core + 2.5 + +``` + +(这个 artifactId 还是 log4j,没有体现出来 2,而在 version 中体现,多少叫人误以为是 log4j) + +**第二步**,来个最简单的测试用例: + +```java +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class Demo { + private static final Logger logger = LogManager.getLogger(Demo.class); + public static void main(String[] args) { + logger.debug("log4j2"); + } +} +``` + +运行 Demo 类,可以在控制台看到以下信息: + +``` +ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. +``` + +Log4j 2 竟然没有在控制台打印“ log4j2”,还抱怨我们没有为它指定配置文件。在这一点上,我就觉得它没有 Logback 好,毕竟人家会输出。 + +这对于新手来说,很不友好,因为新手在遇到这种情况的时候,往往不知所措。日志里面虽然体现了 ERROR,但代码并没有编译出错或者运行出错,凭什么你不输出? + +那作为编程老鸟来说,我得告诉你,这时候最好探究一下为什么。怎么做呢? + +我们可以复制一下日志信息中的关键字,比如说:“No log4j2 configuration file found”,然后在 Intellij IDEA 中搜一下,如果你下载了源码和文档的话,不除意外,你会在 ConfigurationFactory 类中搜到这段话。 + +可以在方法中打个断点,然后 debug 一下,你就会看到下图中的内容。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-4ba440d9-c0b6-4ad2-b538-9d303cc99d90.png) + +通过源码,你可以看得到,Log4j 2 会去寻找 4 种类型的配置文件,后缀分别是 properties、yaml、json 和 xml。前缀是 log4j2-test 或者 log4j2。 + +得到这个提示后,就可以进行第三步了。 + +**第三步,**在 resource 目录下增加 log4j2-test.xml 文件(方便和 Logback 做对比),内容如下所示: + +```xml + + + + + + + + + + + + + +``` + +Log4j 2 的配置文件格式和 Logback 有点相似,基本的结构为 `< Configuration>` 元素,包含 0 或多个 `< Appenders>` 元素,其后跟 0 或多个 `< Loggers>` 元素,里面再跟最多只能存在一个的 `< Root>` 元素。 + +**1)配置 appender**,也就是配置日志的输出目的地。 + +有 Console,典型的控制台配置信息上面你也看到了,我来简单解释一下里面 pattern 的格式: + +- `%d{HH:mm:ss.SSS}` 表示输出到毫秒的时间 + +- `%t` 输出当前线程名称 + +- `%-5level` 输出日志级别,-5 表示左对齐并且固定输出 5 个字符,如果不足在右边补空格 + +- `%logger` 输出 logger 名称,最多 36 个字符 + +- `%msg` 日志文本 + +- `%n` 换行 + +顺带补充一下其他常用的占位符: + +- `%F` 输出所在的类文件名,如 Demo.java + +- `%L` 输出行号 + +- `%M` 输出所在方法名 + +- `%l` 输出语句所在的行数, 包括类名、方法名、文件名、行数 + +- `%p` 输出日志级别 + +- `%c` 输出包名,如果后面跟有 `{length.}` 参数,比如说 `%c{1.}`,它将输出报名的第一个字符,如 `com.itwanger` 的实际报名将只输出 `c.i` + +再次运行 Demo 类,就可以在控制台看到打印的日志信息了: + +``` +10:14:04.657 [main] DEBUG com.itwanger.Demo - log4j2 +``` + +**2)配置 Loggers**,指定 Root 的日志级别,并且指定具体启用哪一个 Appenders。 + +**3)自动重载配置**。 + +Logback 支持自动重载配置,Log4j 2 也支持,那想要启用这个功能也非常简单,只需要在 Configuration 元素上添加 `monitorInterval` 属性即可。 + +``` + +... + +``` + +注意值要设置成非零,上例中的意思是至少 30 秒后检查配置文件中的更改。最小间隔为 5 秒。 + +### 03、Async 示例 + +除了 Console,还有 Async,可以配合文件的方式来异步写入,典型的配置信息如下所示: + +``` + + + + + %d %p %c [%t] %m%n + + + + + + + + + + + + +``` + +对比 Logback 的配置文件来看,Log4j 2 真的复杂了一些,不太好用,就这么直白地说吧!但自己约的,含着泪也得打完啊。把这个 Async 加入到 Appenders: + +``` + + + + + + + + %d %p %c [%t] %m%n + + + + + + + + + + + + + +``` + +再次运行 Demo 类,可以在项目根路径下看到一个 debug.log 文件,内容如下所示: + +``` +2020-10-30 09:35:49,705 DEBUG com.itwanger.Demo [main] log4j2 +``` + +### 04、RollingFile 示例 + +当然了,Log4j 和 Logback 我们都配置了 RollingFile,Log4j 2 也少不了。RollingFile 会根据 Triggering(触发)策略和 Rollover(过渡)策略来进行日志文件滚动。如果没有配置 Rollover,则使用 DefaultRolloverStrategy 来作为 RollingFile 的默认配置。 + +触发策略包含有,基于 cron 表达式(源于希腊语,时间的意思,用来配置定期执行任务的时间格式)的 CronTriggeringPolicy;基于文件大小的 SizeBasedTriggeringPolicy;基于时间的 TimeBasedTriggeringPolicy。 + +过渡策略包含有,默认的过渡策略 DefaultRolloverStrategy,直接写入的 DirectWriteRolloverStrategy。一般情况下,采用默认的过渡策略即可,它已经足够强大。 + +来看第一个基于 SizeBasedTriggeringPolicy 和 TimeBasedTriggeringPolicy 策略,以及缺省 DefaultRolloverStrategy 策略的配置示例: + +``` + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + + + + + +``` + +为了验证文件的滚动策略,我们调整一下 Demo 类,让它多打印点日志: + +``` +for (int i = 1;i < 20; i++) { + logger.debug("微信搜索「{}」,回复关键字「{}」,有惊喜哦","沉默王二", "java"); +} +``` + +再次运行 Demo 类,可以看到根目录下多了 3 个日志文件: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-07af98ca-cf94-427e-adb6-bd935e32a8d0.png) + + +结合日志文件名,再来看 RollingFile 的配置,就很容易理解了。 + +1)fileName 用来指定文件名。 + +2)filePattern 用来指定文件名的模式,它取决于过渡策略。 + +由于配置文件中没有显式指定过渡策略,因此 RollingFile 会启用默认的 DefaultRolloverStrategy。 + +先来看一下 DefaultRolloverStrategy 的属性: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-32b853ce-8beb-496b-b66f-31b650c257ab.png) + +再来看 filePattern 的值 `rolling-%d{yyyy-MM-dd}-%i.log`,其中 `%d{yyyy-MM-dd}` 很好理解,就是年月日;其中 `%i` 是什么意思呢? + +第一个日志文件名为 rolling.log(最近的日志放在这个里面),第二个文件名除去日期为 rolling-1.log,第二个文件名除去日期为 rolling-2.log,根据这些信息,你能猜到其中的规律吗? + +其实和 DefaultRolloverStrategy 中的 max 属性有关,目前使用的默认值,也就是 7,那就当 rolling-8.log 要生成的时候,删除 rolling-1.log。可以调整 Demo 中的日志输出量来进行验证。 + + + +3)SizeBasedTriggeringPolicy,基于日志文件大小的时间策略,大小以字节为单位,后缀可以是 KB,MB 或 GB,例如 20 MB。 + +再来看一个日志文件压缩的示例,来看配置: + +``` + + + %d %p %c{1.} [%t] %m%n + + + + + +``` + +- fileName 的属性值中包含了一个目录 gz,也就是说日志文件都将放在这个目录下。 + +- filePattern 的属性值中增加了一个 gz 的后缀,这就表明日志文件要进行压缩了,还可以是 zip 格式。 + +运行 Demo 后,可以在 gz 目录下看到以下文件: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-1b04167d-a11f-4447-9062-cb3cdd59aa73.png) + +到此为止,Log4j 2 的基本使用示例就已经完成了。测试环境搞定,我去问一下老板,要不要在生产环境下使用 Log4j 2。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/gongju/logback.md b/docs/gongju/logback.md new file mode 100644 index 0000000000..e96800491f --- /dev/null +++ b/docs/gongju/logback.md @@ -0,0 +1,414 @@ +--- +title: Logback:Spring Boot内置的日志处理框架 +category: + - Java企业级开发 +tag: + - 辅助工具/轮子 +--- + +就在昨天,老板听我说完 Logback 有多牛逼之后,彻底动心了,对我下了死命令,“这么好的日志系统,你还不赶紧点,把它切换到咱的项目当中!” + +我们项目之前用的 Log4j,在我看来,已经足够用了,毕竟是小公司,性能上的要求没那么苛刻。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-320329e9-a754-427f-8a19-2e4f809b6a6f.png) + +[Log4j](https://mp.weixin.qq.com/s/AXgNnJe8djD901EmhFkWUg) 介绍过了,[SLF4J](https://mp.weixin.qq.com/s/EhKf1rHWL-QII0f6eo0uVA) 也介绍过了,那接下来,你懂的,Logback 就要隆重地登场了,毕竟它哥仨有一个爹,那就是巨佬 Ceki Gulcu。 + +### 01、Logback 强在哪 + +1)非常自然地实现了 SLF4J,不需要像 Log4j 和 JUL 那样加一个适配层。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-6ba1b465-5533-49dd-b875-48a10ba29f8e.png) + +2)Spring Boot 的默认日志框架使用的是 Logback。一旦某款工具库成为了默认选项,那就说明这款工具已经超过了其他竞品。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-2696cd8b-7e8c-4476-9a06-272fd22fa4b6.png) + +注意看下图(证据找到了,来自 [Spring Boot 官网](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-logging)): + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-82d78a15-8ae0-4377-a7af-aebd5cda4fda.png) + +也可以通过源码的形式看得到: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-2df2d06e-1b01-428b-8444-d765056e25bb.png) + + +3)支持自动重新加载配置文件,不需要另外创建扫描线程来监视。 + +4)既然是巨佬的新作,那必然在性能上有了很大的提升,不然呢? + +### 02、Logback 使用示例 + +**第一步**,在 pom.xml 文件中添加 Logback 的依赖: + +```xml + + ch.qos.logback + logback-classic + 1.2.3 + +``` + +Maven 会自动导入另外两个依赖: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-1f7d8e00-4be6-4863-940c-037862ad2c41.png) + +logback-core 是 Logback 的核心,logback-classic 是 SLF4J 的实现。 + +**第二步**,来个最简单的测试用例: + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Test { + static Logger logger = LoggerFactory.getLogger(Test.class); + public static void main(String[] args) { + logger.debug("logback"); + } +} +``` + +Logger 和 LoggerFactory 都来自 SLF4J,所以如果项目是从 Log4j + SLF4J 切换到 Logback 的话,此时的代码是零改动的。 + +运行 Test 类,可以在控制台看到以下信息: + +``` +12:04:20.149 [main] DEBUG com.itwanger.Test - logback +``` + +在没有配置文件的情况下,一切都是默认的,Logback 的日志信息会输出到控制台。可以通过 StatusPrinter 来打印 Logback 的内部信息: + +```java +LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory(); +StatusPrinter.print(lc); +``` + +在 main 方法中添加以上代码后,再次运行 Test 类,可以在控制台看到以下信息: + +``` +12:59:22.314 [main] DEBUG com.itwanger.Test - logback +12:59:22,261 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml] +12:59:22,262 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy] +12:59:22,262 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.xml] +12:59:22,268 |-INFO in ch.qos.logback.classic.BasicConfigurator@5e853265 - Setting up default configuration. +``` + +也就是说,Logback 会在 classpath 路径下先寻找 logback-test.xml 文件,没有找到的话,寻找 logback.groovy 文件,还没有的话,寻找 logback.xml 文件,都找不到的话,就输出到控制台。 + +一般来说,我们会在本地环境中配置 logback-test.xml,在生产环境下配置 logback.xml。 + +**第三步**,在 resource 目录下增加 logback-test.xml 文件,内容如下所示: + +```xml + + + + %d{HH:mm:ss.SSS} %relative [%thread] %-5level %logger{36} - %msg%n + + + + + + + +``` + +Logback 的配置文件非常灵活,最基本的结构为 `` 元素,包含 0 或多个 `` 元素,其后跟 0 或多个 `` 元素,其后再跟最多只能存在一个的 `` 元素。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-b81ab795-2a2c-44c3-a4b8-b96ef78dcd88.png) + + +**1)配置 appender**,也就是配置日志的输出目的地,通过 name 属性指定名字,通过 class 属性指定目的地: + +- ch.qos.logback.core.ConsoleAppender:输出到控制台。 +- ch.qos.logback.core.FileAppender:输出到文件。 +- ch.qos.logback.core.rolling.RollingFileAppender:文件大小超过阈值时产生一个新文件。 + +除了输出到本地,还可以通过 SocketAppender 和 SSLSocketAppender 输出到远程设备,通过 SMTPAppender 输出到邮件。甚至可以通过 DBAppender 输出到数据库中。 + +encoder 负责把日志信息转换成字节数组,并且把字节数组写到输出流。 + +pattern 用来指定日志的输出格式: + +- `%d`:输出的时间格式。 +- `%thread`:日志的线程名。 +- `%-5level`:日志的输出级别,填充到 5 个字符。比如说 info 只有 4 个字符,就填充一个空格,这样日志信息就对齐了。 + +反例(没有指定 -5 的情况): + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-b30bc0ca-5c78-4853-922b-36bb0c7d8628.png) + + +- `%logger{length}`:logger 的名称,length 用来缩短名称。没有指定表示完整输出;0 表示只输出 logger 最右边点号之后的字符串;其他数字表示输出小数点最后边点号之前的字符数量。 +- `%msg`:日志的具体信息。 +- `%n`:换行符。 +- `%relative`:输出从程序启动到创建日志记录的时间,单位为毫秒。 + +**2)配置 root**,它只支持一个属性——level,值可以为:TRACE、DEBUG、INFO、WARN、ERROR、ALL、OFF。 + +appender-ref 用来指定具体的 appender。 + +**3)查看内部状态信息**。 + +可以在代码中通过 StatusPrinter 来打印 Logback 内部状态信息,也可以通过在 configuration 上开启 debug 来打印内部状态信息。 + +重新运行 Test 类,可以在控制台看到以下信息: + +``` +13:54:54,718 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback-test.xml] at [file:/Users/maweiqing/Documents/GitHub/JavaPointNew/codes/logbackDemo/target/classes/logback-test.xml] +13:54:54,826 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender] +13:54:54,828 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT] +13:54:54,833 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property +13:54:54,850 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to DEBUG +13:54:54,850 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT] +13:54:54,850 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration. +13:54:54,851 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@f8c1ddd - Registering current configuration as safe fallback point +13:54:54.853 [main] DEBUG com.itwanger.Test - logback +``` + +**4)自动重载配置**。 + +之前提到 Logback 很强的一个功能就是支持自动重载配置,那想要启用这个功能也非常简单,只需要在 configuration 元素上添加 `scan=true` 即可。 + +``` + + ... + +``` + +默认情况下,扫描的时间间隔是一分钟一次。如果想要调整时间间隔,可以通过 scanPeriod 属性进行调整,单位可以是毫秒(milliseconds)、秒(seconds)、分钟(minutes)或者小时(hours)。 + +下面这个示例指定的时间间隔是 30 秒: + +``` + +``` + +注意:如果指定了时间间隔,没有指定时间单位,默认的时间单位为毫秒。 + +当设置 `scan=true` 后,Logback 会起一个 ReconfigureOnChangeTask 的任务来监视配置文件的变化。 + +### 03、把 log4j.properties 转成 logback-test.xml + +如果你的项目以前用的 Log4j,那么可以通过下面这个网址把 log4j.properties 转成 logback-test.xml: + +>[http://logback.qos.ch/translator/](http://logback.qos.ch/translator/) + +把之前 log4j.properties 的内容拷贝一份: + +``` +### 设置### +log4j.rootLogger = debug,stdout,D,E + +### 输出信息到控制台 ### +log4j.appender.stdout = org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target = System.out +log4j.appender.stdout.layout = org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n + +### 输出DEBUG 级别以上的日志到=debug.log ### +log4j.appender.D = org.apache.log4j.DailyRollingFileAppender +log4j.appender.D.File = debug.log +log4j.appender.D.Append = true +log4j.appender.D.Threshold = DEBUG +log4j.appender.D.layout = org.apache.log4j.PatternLayout +log4j.appender.D.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n + +### 输出ERROR 级别以上的日志到=error.log ### +log4j.appender.E = org.apache.log4j.DailyRollingFileAppender +log4j.appender.E.File =error.log +log4j.appender.E.Append = true +log4j.appender.E.Threshold = ERROR +log4j.appender.E.layout = org.apache.log4j.PatternLayout +log4j.appender.E.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n +``` + +粘贴到该网址的文本域: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-6c934584-3624-4f40-8108-13bfffc0c40b.png) + +点击「Translate」,可以得到以下内容: + +``` + + + + + + + + + + + + System.out + + [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n + + + + + + + true + debug.log + + %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n + + + DEBUG + + + + + + + error.log + true + + %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n + + + ERROR + + + + + + + + +``` + +可以确认一下内容,发现三个 appender 都在。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-7a0edcdf-8706-4a83-9c09-413fc07967ad.png) + +但是呢,转换后的文件并不能直接使用,需要稍微做一些调整,因为: + +第一,日志的格式化有细微的不同,Logback 中没有 `%l`。 + +第二,RollingFileAppender 需要指定 RollingPolicy 和 TriggeringPolicy,前者负责日志的滚动功能,后者负责日志滚动的时机。如果 RollingPolicy 也实现了 TriggeringPolicy 接口,那么只需要设置 RollingPolicy 就好了。 + +TimeBasedRollingPolicy 和 SizeAndTimeBasedRollingPolicy 是两种最常用的滚动策略。 + +TimeBasedRollingPolicy 同时实现了 RollingPolicy 与 TriggeringPolicy 接口,因此使用 TimeBasedRollingPolicy 的时候就可以不指定 TriggeringPolicy。 + +TimeBasedRollingPolicy 可以指定以下属性: + +- fileNamePattern,用来定义文件的名字(必选项)。它的值应该由文件名加上一个 `%d` 的占位符。`%d` 应该包含 `java.text.SimpleDateFormat` 中规定的日期格式,缺省是 `yyyy-MM-dd`。滚动周期是通过 fileNamePattern 推断出来的。 + +- maxHistory,最多保留多少数量的日志文件(可选项),将会通过异步的方式删除旧的文件。比如,你指定按月滚动,指定 `maxHistory = 6`,那么 6 个月内的日志文件将会保留,超过 6 个月的将会被删除。 + +- totalSizeCap,所有日志文件的大小(可选项)。超出这个大小时,旧的日志文件将会被异步删除。需要配合 maxHistory 属性一起使用,并且是第二条件。 + +来看下面这个 RollingFileAppender 配置: + +``` + + debug.log + + + debug.%d{yyyy-MM-dd}.log + + 30 + 3GB + + + %relative [%thread] %level %logger{35} - %msg%n + + +``` + +基于按天滚动的文件策略,最多保留 30 天,最大大小为 30G。 + +SizeAndTimeBasedRollingPolicy 比 TimeBasedRollingPolicy 多了一个日志文件大小设定的属性:maxFileSize,其他完全一样。 + +基于我们对 RollingPolicy 的了解,可以把 logback-test.xml 的内容调整为以下内容: + +``` + + + System.out + + %d{HH:mm:ss.SSS} [%thread] %level %logger{36} - %msg%n + + + + true + debug.log + + + debug.%d{yyyy-MM-dd}.log + + 30 + 3GB + + + %relative [%thread] %-5level %logger{35} - %msg%n + + + + error.log + + + error.%d{yyyy-MM-dd}.log + + 30 + 3GB + + + %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n + + + ERROR + + + + + + + + +``` + +修改 Test 类的内容: + +```java +public class Test { + static Logger logger = LoggerFactory.getLogger(Test.class); + public static void main(String[] args) { + logger.debug("logback"); + logger.error("logback"); + } +} +``` + +运行后,可以在 target 目录下看到两个文件:debug.log 和 errror.log。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/logback-536aa50e-b195-403e-8409-85e4f6966522.png) + +到此为止,项目已经从 Log4j 切换到 Logback 了,过程非常的丝滑顺畅,嘿嘿。 + +### 04、Logback 手册 + +Logback 的官网上是有一份手册的,非常详细,足足 200 多页,只不过是英文版的。小伙伴们可以看完我这篇文章入门实操的 Logback 教程后,到下面的地址看官方手册。 + +>[http://logback.qos.ch/manual/index.html](http://logback.qos.ch/manual/index.html) + +如果英文阅读能力有限的话,可以到 GitHub 上查看雷锋翻译的中文版: + +>[https://github.com/itwanger/logback-chinese-manual](https://github.com/itwanger/logback-chinese-manual) + +当然了,还有一部分小伙伴喜欢看离线版的 PDF,我已经整理好了: + +>链接:[https://pan.baidu.com/s/16FrbwycYUUIfKknlLhRKYA](https://pan.baidu.com/s/16FrbwycYUUIfKknlLhRKYA) 密码:bptl + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/gongju/others.md b/docs/gongju/others.md similarity index 100% rename from docs/src/gongju/others.md rename to docs/gongju/others.md diff --git a/docs/src/gongju/slf4j.md b/docs/gongju/slf4j.md similarity index 86% rename from docs/src/gongju/slf4j.md rename to docs/gongju/slf4j.md index 6dd92aca4b..220072a83e 100644 --- a/docs/src/gongju/slf4j.md +++ b/docs/gongju/slf4j.md @@ -12,11 +12,11 @@ tag: (为什么我把这段文字手敲了下来呢,因为我发现阿里巴巴开发手册上的有语病,瞧下面红色标出的部分) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-94ba034a-c6e6-46e0-bff3-b658bf35945f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-94ba034a-c6e6-46e0-bff3-b658bf35945f.png) (维护和统一,把统一放在最后面读起来真的是别扭,和的有点牵强,请问手册的小编是数学老师教的语文吧?) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-a7a6e0ae-cbee-428e-8a45-2f5a33243625.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-a7a6e0ae-cbee-428e-8a45-2f5a33243625.png) 那看到这条强制性的规约,我就忍不住想要问:“为什么阿里巴巴开发手册会强制使用 SLF4J 作为 Log4J 的门面担当呢?”究竟这背后藏了什么“不可告人”的秘密? @@ -32,11 +32,11 @@ SLF4J 是 Simple Logging Facade for Java 的缩写(for≈4),也就是简 SLF4J 的作者就是 Log4J 和 Logback 的作者,他的 GitHub 主页长下面这样: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-c72cd63d-b15b-401c-8399-ad0355f1f802.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-c72cd63d-b15b-401c-8399-ad0355f1f802.png) 一股秋风瑟瑟的清冷感扑面而来,有没有?可能巨佬不屑于维护他的 GitHub 主页吧?我的 GitHub 主页够凄惨了,没想到巨佬比我还惨,终于可以吹牛逼地说,“我,沉默王二,GitHub 主页比 SLF4J、Log4J 和 Logback 的作者 Ceki Gulcu 绿多了。。。。。。” -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-cdc9e0fb-71ab-42e7-8024-7e9cfd9b30c3.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-cdc9e0fb-71ab-42e7-8024-7e9cfd9b30c3.png) 1996 年初,欧洲安全电子市场项目决定编写自己的跟踪 API,最后该 API 演变成了 Log4j,已经推出就备受宠爱。 @@ -44,17 +44,17 @@ SLF4J 的作者就是 Log4J 和 Logback 的作者,他的 GitHub 主页长下 2002 年 8 月,Apache 就推出了自己的日志包,也就是阿里巴巴开发手册上提到的 JCL(Jakarta Commons Logging)。JCL 的野心很大,它在 JUL 和 Log4j 的基础上提供了一个抽象层的接口,方便使用者在 JUL 和 Log4j 之间切换。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-5b40fac4-0ab1-467d-9dc1-85c43ed879e7.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-5b40fac4-0ab1-467d-9dc1-85c43ed879e7.png) 但 JCL 好像并不怎么招人喜欢,有人是这样抱怨的: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-795d5543-7bd1-450a-8a35-a151be106b3b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-795d5543-7bd1-450a-8a35-a151be106b3b.png) Ceki Gulcu 也觉得 JCL 不好,要不然他也不会在 2005 年自己撸一个名叫 SLF4J 的新项目,对吧?但出来混总是要付出代价的,SLF4J 只有接口,没有实现,总不能强逼着 Java 和 Apache 去实现 SLF4J 接口吧?这太难了,不现实。 但巨佬之所以称之为巨佬,是因为他拥有超出普通人的惊人之处,他在 SLF4J 和 JUL、Log4j、JCL 之间搭了三座桥: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-3044b416-ff14-408f-b933-71993b7ddeee.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-3044b416-ff14-408f-b933-71993b7ddeee.png) 巨佬动手,丰衣足食,有没有?狠起来连自己的 Log4j 都搭个桥。 @@ -70,13 +70,13 @@ Ceki Gulcu 也觉得 JCL 不好,要不然他也不会在 2005 年自己撸一 假设我们正在开发一套系统,打算用 SLF4J 作为门面,Log4j 作为日志系统,我们在项目中使用了 A 框架,而 A 框架的门面是 JCL,日志系统是 JUL,那就相等于要维护两套日志系统,对吧? -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-e66a78a6-1ef6-42c1-86fa-a4c57b3ef160.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-e66a78a6-1ef6-42c1-86fa-a4c57b3ef160.png) 这就难受了! Ceki Gulcu 想到了这个问题,并且帮我们解决了!来看 SLF4J 官网给出的解决方案。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-5e1021d6-6a81-492b-b8d3-f9438014b53b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-5e1021d6-6a81-492b-b8d3-f9438014b53b.png) - 使用 jcl-over-slf4j.jar 替换 commons-logging.jar - 引入 jul-to-slf4j.jar @@ -116,7 +116,7 @@ public class Demo { 调试这段代码的过程中你会发现,Log 的实现有四种: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-a1db024d-1b29-47b2-a1f8-70b899d5b7c0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-a1db024d-1b29-47b2-a1f8-70b899d5b7c0.png) 如果没有绑定 Log4j 的话,就会默认选择 Jdk14Logger——它返回的 Logger 对象,正是 java.util.logging.Logger,也就是 JUL。 @@ -197,15 +197,15 @@ private static Log logger = LogFactory.getLog(Demo.class); SLF4J 除了提供这种解决方案,绑定 Log4j 替换 JUL 和 JCL;还提供了绑定 Logback 替换 JUL、JCL、Log4j 的方案: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-d3162919-47e1-4760-beba-7b77cdf42e71.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-d3162919-47e1-4760-beba-7b77cdf42e71.png) 还有绑定 JUL 替换 JCL 和 Log4j 的方案: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-a7d80721-ec0f-4b99-a59b-15f8344c3819.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-a7d80721-ec0f-4b99-a59b-15f8344c3819.png) 太强了,有木有?有的话请在留言区敲出 666。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-e9233a42-d13e-4d7d-9d9e-b049d08303aa.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-e9233a42-d13e-4d7d-9d9e-b049d08303aa.png) ### 03、SLF4J 比 Log4J 强在哪 @@ -282,7 +282,7 @@ public class Log4jSLF4JDemo { 看到了吧,使用占位符要比“+”操作符方便的多。并且此时不再需要 `isDebugEnabled()` 先进行判断,`debug()` 方法会在字符串拼接之前执行。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-5e831353-b2a3-4a39-80e3-47a044009d95.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-5e831353-b2a3-4a39-80e3-47a044009d95.png) 如果只是 Log4J 的话,会先进行字符串拼接,再执行 `debug()` 方法,来看示例代码: @@ -295,13 +295,13 @@ logger.debug(name + ",年纪:" + age + ",是个非常不要脸的程序员 在调试这段代码的时候,你会发现的,如下图所示: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-4ae3eda7-cd0d-4094-9331-d6070a39c8ea.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-4ae3eda7-cd0d-4094-9331-d6070a39c8ea.png) 这也就意味着,如果日志系统的级别不是 DEBUG,就会多执行了字符串拼接的操作,白白浪费了性能。 注意,阿里巴巴开发手册上还有一条「**强制**」级别的规约: -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-51004734-620a-4c1a-8aa7-6d7f3e1781d6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-51004734-620a-4c1a-8aa7-6d7f3e1781d6.png) 这是因为如果参数是基本数据类型的话,会先进行自动装箱(`Integer.valueOf()`)。测试代码如下所示: @@ -317,7 +317,7 @@ logger.debug("\u6C89\u9ED8\u738B\u4E8C\uFF0C{}\u5C81", Integer.valueOf(18)); 如果参数需要调用其他方法的话,`debug()` 方法会随后调用。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-aec16f40-7849-4a1d-9507-9119707e6c79.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/slf4j-aec16f40-7849-4a1d-9507-9119707e6c79.png) 也就是说,如果不 `isDebugEnabled()` 的话,在不是 DEBUG 级别的情况下,会多执行自动装箱和调用其他方法的操作——程序的性能就下降了! @@ -337,4 +337,4 @@ logger.debug("\u6C89\u9ED8\u738B\u4E8C\uFF0C{}\u5C81", Integer.valueOf(18)); -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/gongju/tabby.md b/docs/gongju/tabby.md new file mode 100644 index 0000000000..c18a7882c1 --- /dev/null +++ b/docs/gongju/tabby.md @@ -0,0 +1,166 @@ +--- +category: + - Java企业级开发 +tag: + - 辅助工具/轮子 +title: Tabby:一款逼格更高的开源终端工具 +--- + +作为一名 Java 后端开发,日常工作中免不了要和 Linux 服务器打交道,因为生产环境基本上都是部署在 Linux 环境下的。以前呢,我会选择 Xshell 来作为终端进行远程操作。 + +随着付费版本的出现,尤其是 Xshell 把 FTP 分离出去后,上传下载文件的话还需要单独装一下 Xftp,这显然没有之前集成在一起方便😖。 + +还有一点让我费解的是,Xshell 竟然一直没有推出 macOS 版。 + +不过,滴水之恩当涌泉相报,我还是要说,Xshell 真的是非常的 Nice,从实习到现在,Windows 环境下,我基本上一直在用,差不多有快 10 年的时间了,感情还是在的。 + +相信很多小伙伴也在问,有没有一款,**集成了 FTP 功能,并且跨平台的终端工具呢?如果能免费开源的话,就更好了**! + +答案是有的,它就是 **Tabby**! + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-01.png) + +GitHub 上已经有 21.4k 的 star 了,这说明 Tabby 非常的受欢迎: + +>[https://github.com/eugeny/tabby](https://github.com/eugeny/tabby) + +*Tabby:二哥,我谢谢你呀,能再吹两句吗?* + +Tabby 是一个高度可定制化的 跨平台的终端工具,支持 Windows、macOS 和 Linux,自带 SFTP 功能,能与 Linux 服务器轻松传输文件,支持多种主题,界面炫酷,插件丰富。 + +### 一、安装 Tabby + +直接到官网 [tabby.sh](tabby.sh) 点击「download」按钮就可以跳转到下载页面,最新的 release 版本是 1.0.164。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-02.png) + +Linux 和 Windows 的比较好选,macOS 分为两个版本,一个是 arm64,一个是 x86-64,什么意思呢? + +这里简单普及下哈。 + +>ARM是英国ARM公司提供一种CPU架构的知识产权,目前主流的手机和平板电脑都采用ARM架构,但 ARM 不生产芯片,只是从各种嵌入式设备、智能手机、平板电脑、智能穿戴和物联网设备体内的上亿颗处理器中“抽成”。 + +Apple M1 是苹果公司的第一款基于ARM架构的自研处理器单片系统。 + +> X86_X64 源于英特尔几十年前出品的CPU型号8086,包括后续型号8088/80286/80386/80486/80586等等,8086以及8088被当时的IBM采用,制造出了名噪一时的IBM PC机,从此个人电脑风靡一时。 + +详情可参阅下面这篇: + +>[https://www.cnblogs.com/zhaoqingqing/p/13145115.html](https://www.cnblogs.com/zhaoqingqing/p/13145115.html) + +从这一点上可以证明,Tabby 的更新是非常勤快的,连 macOS 的最新芯片 M1 都支持了,厉害了呀,我的虎斑猫(Tabby)! + +按照提示,一步步安装就 OK 了。完成后打开,这界面还是非常炫酷的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-03.png) + +## 二、SSH 连接 + +SSH,也就是 Secure Shell(安全外壳协议),是一种加密的网络传输协议,可在不安全的网络中为网络服务提供安全的传输环境,通过在网络中创建安全隧道来实现 SSH 客户端和服务器端之间的连接。 + +之前说要带大家玩转 Linux 服务器,我们先安装了[宝塔面板](https://mp.weixin.qq.com/s/ditN9J80rSWwnYRumwb4ww)这个神器。宝塔里面有自带的终端,但说实话,体验一般。 + +那不妨我们就使用 Tabby 来与服务器建立一个 SSH 连接吧。 + +点击「setting」→「profiles & connections」→「new profile」。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-04.png) + +填写服务器的 IP 地址和密码,然后点击「save」。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-05.png) + +之后点击「运行」按钮,就可以进入到终端页面了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-06.png) + + +好了,现在可以对服务器进行操作了,执行下 top 命令可以查看服务器上正在运行的进程信息。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-07.png) + +### 三、SFTP 传输文件 + +Tabby 集成了 SFTP,所以上传下载文件就变得非常的简单。只需要点击一下「SFTP」图标就可以打开文件传输窗口。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-08.png) + +上传的时候支持拖拽,完成后会弹出文件传输成功的提示消息。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-09.png) + +下载的时候点击要下载的文件,然后会弹出存储对话框,选择对应的文件夹,以及修改对应的文件名点击「存储」就可以了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-10.png) + +### 四、配置 Tabby + +「Settings」 的面板下有一个「Appearance」的菜单,可以对 Tabby 的外观进行设置,比如说调整字体,比如说自定义样式。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-11.png) + +「Appearance」的菜单可以对 Tabby 的配色方案进行修改,里面的主题非常多,不过我感觉默认的就挺不错,毕竟是官方推荐的。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-12.png) + + 「Plugins」 菜单中还有不少插件可供扩展。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-13.png) + +* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - 使终端中的路径和 URL 可点击 +* [docker](https://github.com/Eugeny/tabby-docker) - 连接到 Docker 容器 +* [title-control](https://github.com/kbjr/terminus-title-control) - 允许通过提供要删除的前缀、后缀和/或字符串来修改终端选项卡的标题 +* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - 快速向一个或所有终端选项卡发送命令 +* [save-output](https://github.com/Eugeny/tabby-save-output) - 将终端输出记录到文件中 + +这里重点说一下「sync config」 这个插件,可以将配置同步到Github或者Gitee的插件。点击「Get」就可以安装,之后会提示你重启生效。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-14.png) + +生效后点击「Sync Config」菜单,就可以看到配置项了,类型可以选择 GitHub、Gitee、GitLab。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-15.png) + +这里以 Gitee 为例,进入个人 Gitee 主页,左侧菜单中选择「私人令牌」,然后点击「生成新令牌」。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-16.png) + +提交后会生成 token,复制到 Tabby 的 Token 输入框中,然后点击「Upload config」,就可以看到配置信息同步成功了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-17.png) + + + + 「Window」 菜单中可以对当前窗口进行设置,比如说改变窗口的主题为 Paper,改变 tab 的位置到底部等等。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/tabby-18.png) + +### 五、总结 + +SSH 连接和 SFTP 传输恐怕是我们操作 Linux 服务器最常用的两个功能了,那 Tabby 对这两个功能的支持非常的友好,足够的轻量级。关键它是跨平台的,Windows、macOS 都可以用,再把配置信息同步到云上后,多平台下切换起来简直不要太舒服。 + +Windows 用户习惯用 Xshell,macOS 用户习惯用 iTerm2,但这两款工具都没办法跨平台,多平台操作的用户就可以选择 Tabby 来体验一下,真心不错。 + +Tabby 的学习资料还比较少,所以希望二哥的这篇文章能给有需要的小伙伴提供一点点的帮助和启发。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + + + + + + + + + + + + + + + + + + diff --git a/docs/gongju/warp.md b/docs/gongju/warp.md new file mode 100644 index 0000000000..6996d814aa --- /dev/null +++ b/docs/gongju/warp.md @@ -0,0 +1,175 @@ +--- +title: Warp:一款21世纪人用的终端工具 +category: + - Java企业级开发 +tag: + - 辅助工具/轮子 +--- + +程序员的一生,用的最多的两个工具,一个是代码编辑器(Code Editor),另外一个就是命令行终端工具(Terminal)。这两个工具对于提高开发效率至关重要。 + +代码编辑器在过去的 40 年里不断进化,从我上大学敲 Java 代码开始,就经历了 MyEclipse、NetBeans、Eclipse,到如今称王称霸的 Intellij IDEA。 + +但终端工具,基本上和上个世纪七八十年代差不多。 + +那本期给大家推荐的这款终端——Warp——绝对会让你大开眼界,用完爱不释手! + +>还记得之前给大家推荐的 [Tabby](https://mp.weixin.qq.com/s/HeUAPe4LqqjfzIeWDe8KIg) 吗?是时候喜新厌旧了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-e0411889-e506-480f-a719-eba4f2d229b4.png) + +Warp,一个超级牛叉的 terminal,号称是 21 世纪的终端,还未正式发布,就获得了两千三百万美元的融资。 + +>官方网站:[https://www.warp.dev/](https://www.warp.dev/) + +Warp 在 GitHub 上也已经开源,目前已经有 2.8k+ 的 star 了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-17a2270c-3bd1-47eb-a205-b7defde42895.png) + +>GitHub 地址:[https://github.com/warpdotdev/Warp](https://github.com/warpdotdev/Warp) + + +Warp 号称自己“Reinvent the Terminal”,也就是重新定义了终端,用过 vscode 的小伙伴是不是对这句口号似曾相识? + +是的,vscode 号称自己“Code editing Redefined”,也就是重新定义了代码编辑器。 + +### 一、安装 Warp + +直接到官网 `warp.dev` 点击「download now」就可以下载最新版了。下载完成后,双击安装包就可以安装了。完成后打开,界面还是非常清爽的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-188834c7-70b7-4f9c-a817-b4a691625fd1.png) + +Warp 支持 GitHub 账户登录。不过,如果你在登录的过程中因为某些原因无法完成跳转,可以通过下面的链接自行解决。 + +>[https://embiid.blog/post/WARP-does-not-work-after-submitting-an-invite-code/](https://embiid.blog/post/WARP-does-not-work-after-submitting-an-invite-code/) + +如果顺利登录,会跳转到这个页面。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-84c0b513-57f3-4ab4-8c77-508c10c923c5.png) + +填写一些 Warp 的调查信息后,就会跳转到 Warp 的初始界面。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-304639e9-7554-45b4-a199-e7c0c3b40c33.png) + +>需要注意的是,Warp 目前仅支持 macOS 版,Linux 和 Windows 用户还需要等待一段时间。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-a4d43a50-0ad9-4f91-ad4e-c1c0788bb580.png) + +其实 macOS 版也是刚刚公测,我这份攻略绝壁是热乎乎的。想要第一时间关注 Warp 版本信息的话,可以戳下图中提到的链接填写自己的邮箱。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-f622d505-b136-4b9d-95c5-a6872e1423e1.png) + +### 二、使用 Warp + +Warp 解决的第一个痛点,就是减少配置、方便输入、优化输出,并且增加常用命令的自动提示。 + +**1)智能提示** + +普通的终端在你键入 tab 的时候,是这样提示的,就是简单地帮你罗列下。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-078017c6-a872-466a-8aa2-f202c9371493.png) + +而 Warp 就非常的时髦,会给你滚动可选的列表形式展示出来。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-4e205289-d8c4-49a9-90ba-08aef8beb627.png) + +Warp 的智能提示也更加“智能化”,它会猜测你下一步的命令到底输入什么。 + +比如说我的工作目录下有一个 README.md 的文件,那当我输入 `echo '沉默王二' >>`的时候它会把 `README.md` 提示在后面。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-8948ab59-3ce8-4b04-80a5-ecd7663e1034.png) + +**2)智能记忆** + +Warp 会记录上一次执行的命令,在顶部会有一个提示的按钮,当你点击的时候,它会自动滚动到上一个命令执行的位置。 + +点击「clear」之前。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-6055bfaa-a146-4cf8-a6f4-aa493dbfa60b.png) + +点击「clear」之后。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-181dff97-bd6f-4c41-94c8-8e9ac5567460.png) + +**3)区域选择** + +传统的终端,在复制区域命令和输出结果的时候需要全部手动选择,而 Warp 是可以点选的,之后可以通过右键菜单进行复制粘贴(可以选择只复制命令或者输出,也可以都选),非常方便。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-23c0a936-2371-4cf4-acf1-555bceecac44.png) + +**4)历史命令** + +传统的终端在通过 up-down 键选择历史命令的时候,一次只能提示一个命令。而 Warp 会把历史命令做成一个滚动的可以选择的列表。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-da43dca3-d8d1-43ad-9308-f8ec1c2b871b.png) + + +**5)命令导航** + +同时按下 Ctrl+Shift+R 可以打开命令导航,Warp 集成了很多工具的命令导航。比如说我们要执行 `git reset` 命令,那么到底格式什么,应该怎么执行,Warp 都提示的非常到位。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-b9a4fd3f-24b2-4f6a-8a70-58fa1b313df3.png) + +这让我想起了 macOS 的效率工具 Alfred,可以搜索任何你想要的命令。 + +**6)AI 植入** + +Warp 还提供了 AI 智能搜索,快捷键可以在 setting→keyboard shortcuts 中找得到,键入 AI 关键字即可。 + +可调整为自己喜欢的快捷键。我目前设置的是 `Ctrl+shift+>`。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-04300bc5-5d0d-494b-955c-1d270133227a.png) + +比如说我问它“how many lines were changed in the last 2 commits?” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-871e51e9-c2ac-4ecb-bfcb-ff339f05bd61.png) + +Warp 解决的第二个痛点是增加协作功能。不过由于我目前没有邀请其他用户参与,还无法使用共享功能,后面有小伙伴体验的话,可以通过我分享的链接下载试一波。 + +>https://app.warp.dev/referral/25KR3Y + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-d8952a84-a0a7-4c3d-b237-87cdc997bb4c.png) + +### 三、配置 Warp + +输入 Command+P 快捷键可以打开 Warp 的命令面板。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-369d16f6-f897-4c9f-bbea-631d561e145b.png) + +键入 `sett` 关键字就可以打开配置页。 + +比如说在「Appearance」选项卡里可以设置 Warp 的主题、字体,以及紧凑型模式。 + +大概有十多种主题可选,比如说这个女生非常喜欢的粉色系。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-6b5dbf4c-7bb0-4926-b9e9-b64333cc2ed8.png) + +更多主题可以到 GitHub 仓库的 theme 页。 + +>[https://github.com/warpdotdev/themes](https://github.com/warpdotdev/themes) + +至于快捷键配置,如果不确定有哪些快捷键可以尝试,直接点击 Warp 顶部的这个温馨提示「welcome tips」就可以了。 + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/warp-aa785da8-bb39-4851-97f5-b7f8baaccf34.png) + + +### 四、总结 + +最后总结一波吧。 + +这波着实属于尝鲜了,市面上应该还木有 Warp 终端的普及安利文章,我这期应该属于大姑娘坐花轿———头一回。 + +害,登录折腾了好久,原因我就不多说了,小伙伴们自行体会哈。反正我是没被劝退。 + +幸好是没放弃,所以才体验到了 Warp 的强大之处,真的是改变了我对终端 terminal 的认知——太特喵的炫酷了! + +这个过程就有点陶渊明《桃花源记》里那句“初极狭,复行数十步,豁然开朗”的赶脚。 + +喜欢的小伙伴一定要尝试一把,你会来感谢我的。好了,这期就先聊到这吧,毕竟 Warp 刚公测,后面有机会再来给大家详细地说。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/home.md b/docs/home.md new file mode 100644 index 0000000000..c3f6195768 --- /dev/null +++ b/docs/home.md @@ -0,0 +1,451 @@ +--- +title: Java程序员进阶之路x沉默王二 +isOriginal: true +headerDepth: 1 +--- + +👉 沉默王二-《Java 程序员进阶之路》官方知识星球来啦!!!如果你需要专属Java学习/面试小册/一对一交流/简历修改/专属求职指南/学习打卡,不妨花 3 分钟左右看看星球的详细介绍: 《Java 程序员进阶之路》详细介绍 (一定要确定自己真的需要再加入,一定要看完详细介绍之后再加我)。 + +::: center +

+ + Java 程序员进阶之路 + +

+ +

+ + + + + 无套路下载 + +

+::: + + +## 为什么会有这个开源知识库 + + +知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 Java 程序员进阶之路😄。 + + 知识库旨在为学习 Java 的小伙伴提供一系列: + - **优质的原创 Java 教程** + - **全面清晰的 Java 学习路线** + - **免费但靠谱的 Java 学习资料** + - **精选的 Java 岗求职面试指南** + - **Java 企业级开发所需的必备技术** + +赠人玫瑰手有余香。知识库会持续保持**更新**,欢迎收藏品鉴! + +**转载须知** :以下所有文章如非文首说明为转载皆为我(沉默王二)的原创,转载在文首注明出处,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! + +推荐你通过在线阅读网站进行阅读,体验更好,速度更快! + +- [Java 程序员进阶之路在线阅读网站(VuePress 版)](https://tobebetterjavaer.com/) +- [Java 程序员进阶之路在线阅读网站(docsify 版)](https://docsify.tobebetterjavaer.com/) + +亿点点小请求,建议戳[这个链接🔗](https://github.com/itwanger/toBeBetterJavaer)给本仓库点个 star,满足一下我的虚荣心,内容质量也绝对值得你一个 star。我还在继续创作,给我一点继续更新的动力,笔芯。 + +## 知识库地图 + + +知识库收录的核心内容就全在这里面了,大类分为 Java 核心、Java 企业级开发、数据库、计算机基础、求职面试、学习资源、程序人生,几乎你需要的这里都有。 + +![](https://img-blog.csdnimg.cn/7fa7cbd99d6449288f4e75cb7d771ec4.png) + + +## 学习路线 + +除了 Java 学习路线,还有 C语言、C++、Python、Go 语言、操作系统、前端、蓝桥杯等硬核学习路线,欢迎收藏品鉴! + +* [Java学习路线一条龙版(建议收藏:+1:)](xuexiluxian/java/yitiaolong.md) +* [Java并发编程学习路线(建议收藏:+1:)](xuexiluxian/java/thread.md) +* [Java虚拟机学习路线(建议收藏:+1:)](xuexiluxian/java/jvm.md) +* [C语言学习路线(建议收藏:+1:)](xuexiluxian/c.md) +* [C++学习路线(建议收藏:+1:)](xuexiluxian/ccc.md) +* [Python学习路线(建议收藏:+1:)](xuexiluxian/python.md) +* [Go语言学习路线(建议收藏:+1:)](xuexiluxian/go.md) +* [操作系统学习路线(建议收藏:+1:)](xuexiluxian/os.md) +* [前端学习路线(建议收藏:+1:)](xuexiluxian/qianduan.md) +* [蓝桥杯学习路线(建议收藏:+1:)](xuexiluxian/lanqiaobei.md) +* [算法和数据结构学习路线(建议收藏:+1:)](xuexiluxian/algorithm.md) + +## 面渣逆袭 + +**面试前必读系列**!包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis 等等。 + +- [面渣逆袭(Java 基础篇)必看:+1:](sidebar/sanfene/javase.md) +- [面渣逆袭(Java 集合框架篇)必看:+1:](sidebar/sanfene/collection.md) +- [面渣逆袭(Java 并发编程篇)必看:+1:](sidebar/sanfene/javathread.md) +- [面渣逆袭(Java 虚拟机篇)必看:+1:](sidebar/sanfene/jvm.md) +- [面渣逆袭(Spring)必看:+1:](sidebar/sanfene/spring.md) +- [面渣逆袭(Redis)必看:+1:](sidebar/sanfene/redis.md) + +## 学习建议 + +**收集了我所有的知乎高赞帖子**!全方位迷茫解惑。 + +- [如何阅读《深入理解计算机系统》这本书?](xuexijianyi/read-csapp.md) +- [电子信息工程最好的出路的是什么?](xuexijianyi/electron-information-engineering.md) + +## Java核心 + +**Java核心非常重要**!我将其分成了Java 基础篇(包括基础语法、面向对象、集合框架、异常处理、Java IO 等)、Java 并发篇和 Java 虚拟机篇。 + +### Java概述 + +- [什么是Java?Java发展简史,Java的优势](overview/what-is-java.md) +- [第一个Java程序:Hello World](overview/hello-world.md) + + +### Java基础语法 + +- [Java支持的8种基本数据类型](basic-grammar/basic-data-type.md) +- [Java流程控制语句](basic-grammar/flow-control.md) +- [Java运算符](basic-grammar/operator.md) +- [Java注释:单行、多行和文档注释](basic-grammar/javadoc.md) +- [Java中常用的48个关键字](basic-extra-meal/48-keywords.md) +- [Java命名规范(非常全面,可以收藏)](basic-extra-meal/java-naming.md) + +### Java面向对象编程 + +- [怎么理解Java中类和对象的概念?](oo/object-class.md) +- [Java变量的作用域:局部变量、成员变量、静态变量、常量](oo/var.md) +- [Java方法](oo/method.md) +- [Java构造方法](oo/construct.md) +- [Java代码初始化块](oo/code-init.md) +- [Java抽象类](oo/abstract.md) +- [Java接口](oo/interface.md) +- [Java内部类](oo/inner-class.md) +- [Java中的static关键字解析](oo/static.md) +- [Java中this和super的用法总结](oo/this-super.md) +- [浅析Java中的final关键字](oo/final.md) +- [Java instanceof关键字用法](oo/instanceof.md) +- [深入理解Java中的不可变对象](basic-extra-meal/immutable.md) +- [Java中可变参数的使用](basic-extra-meal/varables.md) +- [深入理解Java泛型](basic-extra-meal/generic.md) +- [深入理解Java注解](basic-extra-meal/annotation.md) +- [Java枚举(enum)](basic-extra-meal/enum.md) +- [大白话说Java反射:入门、使用、原理](basic-extra-meal/fanshe.md) + +### 字符串&数组 + +- [为什么String是不可变的?](string/immutable.md) +- [深入了解Java字符串常量池](string/constant-pool.md) +- [深入解析 String#intern](string/intern.md) +- [Java判断两个字符串是否相等?](string/equals.md) +- [Java字符串拼接的几种方式](string/join.md) +- [如何在Java中优雅地分割String字符串?](string/split.md) +- [深入理解Java数组](array/array.md) +- [如何优雅地打印Java数组?](array/print.md) + +### 集合框架(容器) + +- [Java集合框架](collection/gailan.md) +- [Java集合ArrayList详解](collection/arraylist.md) +- [Java集合LinkedList详解](collection/linkedlist.md) +- [Java中ArrayList和LinkedList的区别](collection/list-war-2.md) +- [Java中的Iterator和Iterable区别](collection/iterator-iterable.md) +- [为什么阿里巴巴强制不要在foreach里执行删除操作](collection/fail-fast.md) +- [Java8系列之重新认识HashMap](collection/hashmap.md) + +### Java输入输出 + +- [Java IO学习整理](io/shangtou.md) +- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](io/BIONIOAIO.md) + + +### 异常处理 + +- [一文读懂Java异常处理](exception/gailan.md) +- [详解Java7新增的try-with-resouces语法](exception/try-with-resouces.md) +- [Java异常处理的20个最佳实践](exception/shijian.md) +- [Java空指针NullPointerException的传说](exception/npe.md) + +### 常用工具类 + +- [Java Arrays工具类10大常用方法](common-tool/arrays.md) +- [Java集合框架:Collections工具类](common-tool/collections.md) +- [Hutool:国产良心工具包,让你的Java变得更甜](common-tool/hutool.md) +- [Google开源的Guava工具库,太强大了~](common-tool/guava.md) + +### Java新特性 + +- [Java 8 Stream流详细用法](java8/stream.md) +- [Java 8 Optional最佳指南](java8/optional.md) +- [深入浅出Java 8 Lambda表达式](java8/Lambda.md) + +### Java重要知识点 + +- [彻底弄懂Java中的Unicode和UTF-8编码](basic-extra-meal/java-unicode.md) +- [Java中int、Integer、new Integer之间的区别](basic-extra-meal/int-cache.md) +- [深入剖析Java中的拆箱和装箱](basic-extra-meal/box.md) +- [彻底讲明白的Java浅拷贝与深拷贝](basic-extra-meal/deep-copy.md) +- [深入理解Java中的hashCode方法](basic-extra-meal/hashcode.md) +- [一次性搞清楚equals和hashCode](basic-extra-meal/equals-hashcode.md) +- [Java重写(Override)与重载(Overload)](basic-extra-meal/override-overload.md) +- [Java重写(Overriding)时应当遵守的11条规则](basic-extra-meal/Overriding.md) +- [Java到底是值传递还是引用传递?](basic-extra-meal/pass-by-value.md) +- [Java不能实现真正泛型的原因是什么?](basic-extra-meal/true-generic.md) +- [详解Java中Comparable和Comparator的区别](basic-extra-meal/comparable-omparator.md) +- [jdk9为何要将String的底层实现由char[]改成了byte[]?](basic-extra-meal/jdk9-char-byte-string.md) +- [为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)?](basic-extra-meal/jdk-while-for-wuxian-xunhuan.md) +- [先有Class还是先有Object?](basic-extra-meal/class-object.md) +- [instanceof关键字是如何实现的?](basic-extra-meal/instanceof-jvm.md) + + +### Java并发编程 + +- [室友打了一把王者就学会了创建Java线程的3种方式](thread/wangzhe-thread.md) +- [Java线程的6种状态及切换(透彻讲解)](thread/thread-state-and-method.md) +- [线程组是什么?线程优先级如何设置?](thread/thread-group-and-thread-priority.md) +- [进程与线程的区别是什么?](thread/why-need-thread.md) +- [并发编程带来了哪些问题?](thread/thread-bring-some-problem.md) +- [全面理解Java的内存模型(JMM)](thread/jmm.md) +- [Java并发编程volatile关键字解析](thread/volatile.md) +- [Java中的synchronized关键字锁的到底是什么?](thread/synchronized.md) +- [Java实现CAS的原理](thread/cas.md) +- [Java并发AQS详解](thread/aqs.md) +- [大致了解下Java的锁接口和锁](thread/lock.md) +- [深入理解Java并发重入锁ReentrantLock](thread/reentrantLock.md) +- [深入理解Java并发读写锁ReentrantReadWriteLock](thread/ReentrantReadWriteLock.md) +- [深入理解Java并发线程协作类Condition](thread/condition.md) +- [深入理解Java并发线程线程阻塞唤醒类LockSupport](thread/LockSupport.md) +- [简单聊聊Java的并发集合容器](thread/map.md) +- [吊打Java并发面试官之ConcurrentHashMap](thread/ConcurrentHashMap.md) +- [吊打Java并发面试官之ConcurrentLinkedQueue](thread/ConcurrentLinkedQueue.md) +- [吊打Java并发面试官之CopyOnWriteArrayList](thread/CopyOnWriteArrayList.md) +- [吊打Java并发面试官之ThreadLocal](thread/ThreadLocal.md) +- [吊打Java并发面试官之BlockingQueue](thread/BlockingQueue.md) +- [面试必备:Java线程池解析](thread/pool.md) +- [深入剖析Java计划任务ScheduledThreadPoolExecutor](thread/ScheduledThreadPoolExecutor.md) +- [Java atomic包中的原子操作类总结](thread/atomic.md) +- [Java并发编程通信工具类CountDownLatch等一网打尽](thread/CountDownLatch.md) +- [深入理解Java并发编程之Fork/Join框架](thread/fork-join.md) +- [从根上理解生产者-消费者模式](thread/shengchanzhe-xiaofeizhe.md) + + +### Java虚拟机 + +- [什么是JVM?](jvm/what-is-jvm.md) +- [JVM到底是如何运行Java代码的?](jvm/how-run-java-code.md) +- [我竟然不再抗拒Java的类加载机制了](jvm/class-load.md) +- [详解Java的类文件(class文件)结构](jvm/class-file-jiegou.md) +- [从javap的角度轻松看懂字节码](jvm/bytecode.md) +- [JVM字节码指令详解](jvm/zijiema-zhiling.md) +- [虚拟机是如何执行字节码指令的?](jvm/how-jvm-run-zijiema-zhiling.md) +- [HSDB(Hotspot Debugger)从入门到实战](jvm/hsdb.md) +- [史上最通俗易懂的ASM教程](jvm/asm.md) +- [自己编译JDK](jvm/compile-jdk.md) +- [深入理解JVM的内存结构](jvm/neicun-jiegou.md) +- [Java 创建的对象到底放在哪?](jvm/whereis-the-object.md) +- [咱们从头到尾说一次Java垃圾回收](jvm/gc.md) +- [图解Java的垃圾回收机制](jvm/tujie-gc.md) +- [Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析)](jvm/problem-tools.md) +- [Java即时编译(JIT)器原理解析及实践](jvm/jit.md) +- [一次内存溢出排查优化实战](jvm/oom.md) +- [一次生产CPU 100% 排查优化实践](jvm/cpu-percent-100.md) +- [JVM 核心知识点总结](jvm/zongjie.md) + +## Java企业级开发 + + + - **到底能不能成为一名合格的 Java 程序员,从理论走向实战?Java 企业级开发这部分内容就是一个分水岭**! + - 纸上得来终觉浅,须知此事要躬行。 + + +### 开发工具 + +- [终于把项目构建神器Maven捋清楚了~](maven/maven.md) +- [我在工作中是如何使用Git的](git/git-qiyuan.md) +- [5分钟带你深入浅出搞懂Nginx](nginx/nginx.md) + +### IDE/编辑器 + +- [4个高级的IntelliJ IDEA调试技巧](ide/4-debug-skill.md) + +### Spring + +- [Spring AOP扫盲](springboot/aop-log.md) +- [Spring IoC扫盲](springboot/ioc.md) + +### SpringBoot + +- [一分钟快速搭建Spring Boot项目](springboot/initializr.md) +- [Spring Boot 整合 MySQL 和 Druid](springboot/mysql-druid.md) +- [Spring Boot 整合 JPA](springboot/jpa.md) +- [Spring Boot 整合 Thymeleaf 模板引擎](springboot/thymeleaf.md) +- [Spring Boot 如何开启事务支持?](springboot/transaction.md) +- [Spring Boot 中使用过滤器、拦截器、监听器](springboot/Filter-Interceptor-Listener.md) +- [Spring Boot 整合 Redis 实现缓存](redis/redis-springboot.md) +- [Spring Boot 整合 Logback 定制日志框架](springboot/logback.md) +- [Spring Boot 整合 Swagger-UI 实现在线API文档](springboot/swagger.md) +- [Spring Boot 整合 Knife4j,美化强化丑陋的Swagger](gongju/knife4j.md) +- [Spring Boot 整合 Spring Task 实现定时任务](springboot/springtask.md) +- [Spring Boot 整合 MyBatis-Plus AutoGenerator 生成编程喵项目骨架代码](kaiyuan/auto-generator.md) + +## 辅助工具/轮子 + +- [Tabby:一款逼格更高的开源终端工具](gongju/tabby.md) +- [Warp:一款21世纪人用的终端工具](gongju/warp.md) +- [Chocolatey:一款GitHub星标8.2k+的Windows命令行软件管理器](gongju/choco.md) +- [chiner:一款开源的数据库设计神器](gongju/chiner.md) +- [DBeaver:一款免费的数据库操作工具](gongju/DBeaver.md) +- [Forest:一款极简的声明式HTTP调用API框架](gongju/forest.md) +- [Junit:一个开源的Java单元测试框架](gongju/junit.md) +- [fastjson:阿里巴巴开源的JSON解析库](gongju/fastjson.md) +- [Gson:Google开源的JSON解析库](gongju/gson.md) +- [Jackson:GitHub上star数最多的JSON解析库](gongju/jackson.md) +- [Log4j:Java日志框架的鼻祖](gongju/log4j.md) +- [Log4j 2:Apache维护的一款高性能日志记录工具](gongju/log4j2.md) +- [Logback:Spring Boot内置的日志处理框架](gongju/logback.md) +- [SLF4J:阿里巴巴强制使用的日志门面担当](gongju/slf4j.md) + + +### 安全篇 + +### 分布式 + +- [全文搜索引擎Elasticsearch入门教程](elasticsearch/rumen.md) +- [可能是把ZooKeeper概念讲的最清楚的一篇文章](zookeeper/jibenjieshao.md) +- [微服务网关:从对比到选型,由理论到实践](microservice/api-wangguan.md) + +### 高性能 + +#### 消息队列 + +- [RabbitMQ入门教程(概念、应用场景、安装、使用)](mq/rabbitmq-rumen.md) +- [怎么确保消息100%不丢失?](mq/100-budiushi.md) + +### 高可用 + +## 数据库 + + +**简而言之,就是按照数据结构来组织、存储和管理数据的仓库**。几乎所有的 Java 后端开发都要学习数据库这块的知识,包括关系型数据库 MySQL,缓存中间件 Redis,非关系型数据库 MongoDB 等。 + + +### MySQL + +- [如何保障MySQL和Redis的数据一致性?](mysql/redis-shuju-yizhixing.md) + +### Redis + +- [Redis入门(适合新手)](redis/rumen.md) +- [聊聊缓存雪崩、穿透、击穿](redis/xuebeng-chuantou-jichuan.md) + + + +### MongoDB + +- [MongoDB最基础入门教程](mongodb/rumen.md) + + +## 计算机基础 + +**计算机基础包括操作系统、计算机网络、计算机组成原理、数据结构与算法等**。对于任何一名想要走得更远的 Java 后端开发来说,都是必须要花时间和精力去夯实的。万丈高露平地起,勿在浮沙筑高台。 + +- [计算机操作系统知识点大梳理](cs/os.md) +- [计算机网络核心知识点大梳理](cs/wangluo.md) + +## 求职面试 + + +**学习了那么多 Java 知识,耗费了无数的脑细胞,熬掉了无数根秀发,为的是什么?当然是谋取一份心仪的 offer 了**。那八股文、面试题、城市选择、优质面经又怎能少得了呢?千淘万漉虽辛苦,吹尽狂沙始到金。 + + +### 面试题集合 + +- [Java:34道精选高频面试题](baguwen/java-basic-34.md) +- [Java:13道HashMap精选面试题](collection/hashmap-interview.md) +- [Redis:12道精选高频面试题)](mianjing/redis12question.md) +- [Nginx:40道精选面试题](nginx/40-interview.md) + + +### 背诵版八股文 + +- [Java 基础八股文(背诵版)必看:+1:](baguwen/java-basic.md) +- [Java 并发编程八股文(背诵版)必看:+1:](baguwen/java-thread.md) +- [Java 虚拟机八股文(背诵版)必看:+1:](baguwen/jvm.md) +- [MySQL 八股文(背诵版)必看:+1:](sidebar/herongwei/mysql.md) + + +### 优质面经 + +- [春招斩获深圳腾讯PCG和杭州阿里云](mianjing/shanganaliyun.md) +- [社招拿下阿里美团头条京东滴滴)](https://mp.weixin.qq.com/s/h2tV6v5Rh6jHdO9x0p63-g) +- [字节小姐姐的一份秋招攻略](https://mp.weixin.qq.com/s/0hCJy0m8nHm08HfyXKQT1A) +- [面试常见词汇扫盲+常见大厂面试特点分享](https://mp.weixin.qq.com/s/6TYEDM73N68vKXpmLRKhHA) +- [双非学历的社畜,历经 6 轮面试,最终拿下阿里Offer](https://mp.weixin.qq.com/s/vnMZY9Gsy3o1FwMi4f1GlA) + +### 面试准备 + +- [简历如何优化,简历如何投递,面试如何准备?](https://mp.weixin.qq.com/s/qurUqeD_VyiJRtB38vOuSw) +- [校招时间节点、简历编写、笔试、、HR面、实习等注意事项](https://mp.weixin.qq.com/s/rO7cU4NX74CoWADo_O4IUw) + +### 城市选择 + +- [北京都有哪些牛逼的互联网公司?](cityselect/beijing.md) +- [想去广州了!](cityselect/guangzhou.md) +- [深圳有哪些牛批的互联网公司?](cityselect/shenzhen.md) +- [西安有哪些不错的互联网公司?](cityselect/xian.md) +- [青岛有牛逼的互联网公司吗?](cityselect/qingdao.md) +- [郑州有哪些不错的互联网公司?](cityselect/zhengzhou.md) +- [想搬去苏州生活了。。。](cityselect/suzhou.md) +- [南京有哪些靠谱的互联网公司?](cityselect/nanjing.md) +- [杭州有哪些顶级的互联网公司?](cityselect/hangzhou.md) +- [成都有哪些牛批的互联网公司?](cityselect/chengdu.md) + +### 工作体会 + +## 学习资源 + + - **不知道学什么?不知道该怎么学?找不到优质的学习资源**?这些问题在这里统统都可以找到答案。 + - 我会把自己十多年的编程经验和学习资源毫不保留的分享出来。 + +### PDF下载 + +- [👏下载→Java程序员常读书单](download/java.md) +- [👏下载→最全最硬核的Java面试 “备战” 资料](https://mp.weixin.qq.com/s/US5nTxbC2nYc1hWpn5Bozw) +- [👏下载→深入浅出Java多线程](https://mp.weixin.qq.com/s/pxKrjw_5NTdZfHOKCkwn8w) +- [👏下载→GitHub星标115k+的Java教程](https://mp.weixin.qq.com/s/d7Z0QoChNuP9bTwAGh2QCw) +- [👏下载→重学Java设计模式](https://mp.weixin.qq.com/s/PH5AizUAnTz0CuvJclpAKw) +- [👏下载→重学Java设计模式](https://mp.weixin.qq.com/s/PH5AizUAnTz0CuvJclpAKw) +- [👏下载→Java版LeetCode刷题笔记](https://mp.weixin.qq.com/s/FyoOPIMGcaeH0z5RMhxtaQ) +- [👏下载→阮一峰C语言入门教程](download/yuanyifeng-c-language.md) +- [👏下载→BAT大佬的刷题笔记](download/bat-shuati.md) +- [👏下载→给操作系统捋条线](https://mp.weixin.qq.com/s/puTGbgU7xQnRcvz5hxGBHA) +- [👏下载→豆瓣9.1分,Pro Git中文版](download/progit.md) +- [👏下载→简历模板](download/jianli.md) + + +## 知识库搭建历程 + + +从购买阿里云服务器+域名购买+域名备案+HTTP 升级到 HTTPS,全方面记录《Java 程序员进阶之路》知识库的诞生和改进过程,涉及到 docsify、Git、Linux 命令、GitHub 仓库等实用知识点。 + + +- [阿里云服务器购买+宝塔面板安装+域名购买+域名备案+升级HTTPS](szjy/tobebetterjavaer-beian.md) +- [使用docsify+Git+GitHub+码云+阿里云服务器搭建知识库网站](szjy/tobebetterjavaer-wangzhan-shangxian.md) + + +## 联系作者 + + +- 作者是一名普通普通普通普通三连的 Java 后端开发者,热爱学习,热爱分享 +- 参加工作以后越来越理解交流和分享的重要性,在不停地汲取营养的同时,也希望帮助到更多的小伙伴们 +- Java 程序员进阶之路,不仅是作者自学 Java 以来所有的原创文章和学习资料的大聚合,更是作者向这个世界传播知识的一个窗口。 + + +### 心路历程 + +- [走近作者:个人介绍 Q&A](about-the-author/readme.md) +- [我的第一个,10 万(B站视频播放)](about-the-author/bzhan-10wan.md) +- [我的第一个,一千万!知乎阅读](about-the-author/zhihu-1000wan.md) +- [我的第二个,一千万!CSDN阅读](about-the-author/csdn-1000wan.md) + + + + + diff --git a/docs/ide/4-debug-skill.md b/docs/ide/4-debug-skill.md new file mode 100644 index 0000000000..15bbd606c5 --- /dev/null +++ b/docs/ide/4-debug-skill.md @@ -0,0 +1,142 @@ +--- +category: + - Java企业级开发 +tag: + - Intellij IDEA +--- + +# 4个高级的IntelliJ IDEA调试技巧 + + +大家好,我是二哥! + +今天给大家带来几个我日常工作以及阅读源码必备的 IntelliJ IDEA 高级调试技巧,分分钟要起飞的节奏。 + +## 断点处添加 log + +很多程序员在调试代码时都喜欢 `print` 一些内容,这样看起来更直观,print 完之后又很容易忘记删除掉这些没用的内容,最终将代码提交到 `remote`,code review 时又不得不删减这些内容重新提交,不但增加不必要的工作量,还让 `log tree` 的一些节点没有任何价值 + +IntelliJ IDEA 提供 `Evaluate and Log at Breakpoints` 功能恰巧可以帮助我们解决这个问题, 来看下面代码: + +```java +public static void main(String[] args) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int count = 0; + for (int i = 0; i < 5; i++) { + if (isInterested(random.nextInt(10))) { + count++; + } + } + System.out.printf("Found %d interested values%n", count); + } + + private static boolean isInterested(int i) { + return i % 2 == 0; + } +``` + +假如我们想在第 15 行查看每次调用,随即出来的 i 的值到底是多少,我们没必要在这个地方添加任何 log,在正常加断点的地方使用快捷键 `Shift + 鼠标左键`,就会弹出下面的内容 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-e69c965f-f7e5-4e91-a92d-a43a1d0aced4.jpg) + +勾选上 `Evaluate and log`, 并自定义你想查看的 log/变量,比如这里的 `"interested" + i`, 这样以 Debug 模式运行程序(正常模式运行,不会打印这些 log): + +``` +interested 7 +interested 5 +interested 1 +interested 2 +interested 0 +Found 2 interested values +``` + +如果你在多处添加了这种断点,简单的看 log 可能偶尔还是不够直观,可以勾选上面图片绿色框线的 `"Breakpoint hit" message` : + +``` +Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) +interested 6 +Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) +interested 0 +Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) +interested 9 +Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) +interested 8 +Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) +interested 1 +Found 3 interested values +Disconnected from the target VM, address: '127.0.0.1:0', transport: 'socket' + +Process finished with exit code +``` + +如果你想要更详细的信息,那就勾选上 `Stack trace` (大家自己查看运行结果吧),有了这个功能,上面说的一些问题都不复存在了 + +## 字段断点 + +如果你阅读源码,你一定会有个困扰,类中的某个字段的值到底是在哪里改变的,你要一点点追踪调用栈,逐步排查,稍不留神,就可能有遗漏 + +> 我们可以在 IntelliJ IDEA 中为某个字段添加断点,当字段值有修改时,自动跳到相应方法位置 + +使用起来很简单: + +1. 在字段定义处鼠标左键添加断点(会出现「眼睛」的图标) +2. 在「眼睛」图标上鼠标右键 +3. 在弹框中勾选上`Field access` 和`Field modification` 两个选项 + +![image.gif](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-72c23537-3f66-4283-b939-a265b7628a1a.gif) + +如果修改字段值的方法比较多,也可以在 `Condition` 的地方定义断点进入条件, 有了这个功能的加成,相信你阅读源码会顺畅许多 + +## 异常断点 + +除了阅读源码,一定是遇到了异常我们才开始调试代码,代码在抛出异常之后会自动停止,但是我们希望: + +> 代码停在抛出异常之前,方便我们查看当时的变量信息 + +这时我们就用到了 `Exception Breakpoints`, 当抛出异常时,在 catch 的地方打上断点,可以通过下图的几个位置获取栈顶异常类型,比如这里的 `NumberFormatException` + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-c4c511af-b00d-458b-a4a1-97d1fe1e84b8.jpg) + +知道异常类型后,就可以按照如下步骤添加异常断点了: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-4c35cab7-83d2-45b4-8a27-ebeceb41ce08.jpg) + +然后在弹框中选择 NumberFormatException + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-a98e7885-1e84-4c38-8de1-ae04d3013176.gif) + +重新以 Debug 模式运行程序: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-498ad99d-a15d-4a4e-a01b-b0c11cf8f72e.gif) + +程序「一路绿灯式」定位到抛出异常的位置,同时指出当时的变量信息,三个字:稳,准,狠,还有谁? + +## 方法断点 + +当阅读源码时,比如 Spring,一个接口的方法可能被多个子类实现,当运行时,需要查看调用栈逐步定位实现类,IDEA 同样支持在接口方法上添加断点(快捷键 `cmd+F8`/`ctrl+F8`): + +1. 鼠标左键在方法处点击断点(♦️形状) +2. 断点上鼠标右键 + +勾选上绿色框线上的内容,同样可以自定义跳转条件 Condition + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-b81dc459-5a9c-4e0e-b24e-350943299eda.jpg) + +当以 Debug 模式运行程序的时候,会自动进入实现类的方法(注意断点形状): + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-edbc1de2-4dd6-49a3-9a6a-5948d19aabee.jpg) + +看到这你应该想到常见的 Runnable 接口中的 run 方法了,同样是有作用的,大家可以自行去尝试了 + +## 总结 + +相信有以上四种调试技巧的加成,无论是工作debug 还是私下阅读源码,都可以轻松驾驭了。最后,来看看 IDEA 支持的各种断点调试类型,如果你只知道红色小圆点,那咱在留言区好好说说吧 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/ide/4-debug-skill-92ad72da-4bf1-4bc4-b21d-78c33114dc96.jpg) + +----- + +>作者:tan日拱一兵,转载链接:[https://mp.weixin.qq.com/s/KG0yzb_9XhhTSzjHr4DkIQ](https://mp.weixin.qq.com/s/KG0yzb_9XhhTSzjHr4DkIQ) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/io/BIONIOAIO.md b/docs/io/BIONIOAIO.md new file mode 100644 index 0000000000..da6a8fc9f1 --- /dev/null +++ b/docs/io/BIONIOAIO.md @@ -0,0 +1,297 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# 如何给女朋友解释清楚BIO、NIO和AIO? + + +周末午后,在家里面进行电话面试,我问了面试者几个关于 IO 的问题,其中包括什么是 BIO、NIO 和 AIO?三者有什么区别?具体如何使用等问题,但是面试者回答的并不是很满意。于是我在面试评价中写道:"对 Java 的 IO 提醒理解不够深入"。恰好被女朋友看到了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-1.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-2.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-3.gif) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-4.jpg) + +Java IO + +IO,常协作 I/O,是 Input/Output 的简称,即输入/输出。通常指数据在内部存储器(内存)和外部存储器(硬盘、优盘等)或其他周边设备之间的输入和输出。 + +输入/输出是信息处理系统(例如计算机)与外部世界(可能是人类或另一信息处理系统)之间的通信。 + +输入是系统接收的信号或数据,输出则是从其发送的信号或数据。 + +在 Java 中,提供了一系列 API,可以供开发者来读写外部数据或文件。我们称这些 API 为 Java IO。 + +IO 是 Java 中比较重要,且比较难的知识点,主要是因为随着 Java 的发展,目前有三种 IO 共存。分别是 BIO、NIO 和 AIO。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-5.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-6.gif) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-7.gif) + +Java BIO + +BIO 全称Block-IO 是一种**同步且阻塞**的通信模式。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。 + +Java NIO + +Java NIO,全程 Non-Block IO ,是 Java SE 1.4 版以后,针对网络传输效能优化的新功能。是一种**非阻塞同步**的通信模式。 + +NIO 与原来的 I/O 有同样的作用和目的, 他们之间最重要的区别是数据打包和传输的方式。原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 + +面向流的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。 + +面向块的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 + +Java AIO + +Java AIO,全程 Asynchronous IO,是**异步非阻塞**的 IO。是一种非阻塞异步的通信模式。 + +在 NIO 的基础上引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-8.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-9.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-10.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-11.gif) + +三种 IO 的区别 + +首先,我们站在宏观的角度,重新画一下重点: + +**BIO (Blocking I/O):同步阻塞 I/O 模式。** + +**NIO (New I/O):同步非阻塞模式。** + +**AIO (Asynchronous I/O):异步非阻塞 I/O 模型。** + +同步阻塞模式:这种模式下,我们的工作模式是先来到厨房,开始烧水,并坐在水壶面前一直等着水烧开。 + +同步非阻塞模式:这种模式下,我们的工作模式是先来到厨房,开始烧水,但是我们不一直坐在水壶前面等,而是回到客厅看电视,然后每隔几分钟到厨房看一下水有没有烧开。 + +异步非阻塞 I/O 模型:这种模式下,我们的工作模式是先来到厨房,开始烧水,我们不一直坐在水壶前面等,也不隔一段时间去看一下,而是在客厅看电视,水壶上面有个开关,水烧开之后他会通知我。 + +阻塞 VS 非阻塞:人是否坐在水壶前面一直等。 + +同步 VS 异步:水壶是不是在水烧开之后主动通知人。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-12.gif) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-13.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-14.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-15.jpg) + +适用场景 + +BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。 + +NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。 + +AIO 方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-16.gif) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-17.gif) + +使用方式 + +使用 BIO 实现文件的读取和写入。 + +```java +//Initializes The Object +User1 user = new User1(); +user.setName("wanger"); +user.setAge(23); +System.out.println(user); + +//Write Obj to File +ObjectOutputStream oos = null; +try { + oos = new ObjectOutputStream(new FileOutputStream("tempFile")); + oos.writeObject(user); +} catch (IOException e) { + e.printStackTrace(); +} finally { + IOUtils.closeQuietly(oos); +} + +//Read Obj from File +File file = new File("tempFile"); +ObjectInputStream ois = null; +try { + ois = new ObjectInputStream(new FileInputStream(file)); + User1 newUser = (User1) ois.readObject(); + System.out.println(newUser); +} catch (IOException e) { + e.printStackTrace(); +} catch (ClassNotFoundException e) { + e.printStackTrace(); +} finally { + IOUtils.closeQuietly(ois); + try { + FileUtils.forceDelete(file); + } catch (IOException e) { + e.printStackTrace(); + } +} +``` + +使用 NIO 实现文件的读取和写入。 + +```java +static void readNIO() { + String pathname = "C:\\Users\\adew\\Desktop\\jd-gui.cfg"; + FileInputStream fin = null; + try { + fin = new FileInputStream(new File(pathname)); + FileChannel channel = fin.getChannel(); + + int capacity = 100;// 字节 + ByteBuffer bf = ByteBuffer.allocate(capacity); + int length = -1; + + while ((length = channel.read(bf)) != -1) { + + bf.clear(); + byte[] bytes = bf.array(); + System.out.write(bytes, 0, length); + System.out.println(); + } + + channel.close(); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (fin != null) { + try { + fin.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} + +static void writeNIO() { + String filename = "out.txt"; + FileOutputStream fos = null; + try { + + fos = new FileOutputStream(new File(filename)); + FileChannel channel = fos.getChannel(); + ByteBuffer src = Charset.forName("utf8").encode("你好你好你好你好你好"); + int length = 0; + + while ((length = channel.write(src)) != 0) { + System.out.println("写入长度:" + length); + } + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} +``` + +使用AIO实现文件的读取和写入 + +```java +public class ReadFromFile { + public static void main(String[] args) throws Exception { + Path file = Paths.get("/usr/a.txt"); + AsynchronousFileChannel channel = AsynchronousFileChannel.open(file); + + ByteBuffer buffer = ByteBuffer.allocate(100_000); + Future result = channel.read(buffer, 0); + + while (!result.isDone()) { + ProfitCalculator.calculateTax(); + } + Integer bytesRead = result.get(); + System.out.println("Bytes read [" + bytesRead + "]"); + } +} +class ProfitCalculator { + public ProfitCalculator() { + } + public static void calculateTax() { + } +} + +public class WriteToFile { + + public static void main(String[] args) throws Exception { + AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open( + Paths.get("/asynchronous.txt"), StandardOpenOption.READ, + StandardOpenOption.WRITE, StandardOpenOption.CREATE); + CompletionHandler handler = new CompletionHandler() { + + @Override + public void completed(Integer result, Object attachment) { + System.out.println("Attachment: " + attachment + " " + result + + " bytes written"); + System.out.println("CompletionHandler Thread ID: " + + Thread.currentThread().getId()); + } + + @Override + public void failed(Throwable e, Object attachment) { + System.err.println("Attachment: " + attachment + " failed with:"); + e.printStackTrace(); + } + }; + + System.out.println("Main Thread ID: " + Thread.currentThread().getId()); + fileChannel.write(ByteBuffer.wrap("Sample".getBytes()), 0, "First Write", + handler); + fileChannel.write(ByteBuffer.wrap("Box".getBytes()), 0, "Second Write", + handler); + + } +} +``` + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-18.gif) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-19.gif) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-20.gif) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-21.jpg) + +滴滴滴,水开了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-22.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/BIONIOAIO-23.jpg) + + + + + +>转载链接:[https://mp.weixin.qq.com/s/QQxrr5yP8X9YdFqIwXDoQQ](https://mp.weixin.qq.com/s/QQxrr5yP8X9YdFqIwXDoQQ) + + diff --git a/docs/io/shangtou.md b/docs/io/shangtou.md new file mode 100644 index 0000000000..9de843a766 --- /dev/null +++ b/docs/io/shangtou.md @@ -0,0 +1,344 @@ +--- +category: + - Java核心 +tag: + - Java +--- + +# Java IO学习整理 + + +“老王,Java IO 也太上头了吧?”新兵蛋子小二向头顶很凉快的老王抱怨道,“你瞧,我就按照传输方式对 IO 进行了一个简单的分类,就能搞出来这么多的玩意!” + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/shangtou-01.png) + +好久没搞过 IO 了,老王看到这幅思维导图也是吃了一惊。想想也是,他当初学习 Java IO 的时候头也大,乌央乌央的一片,全是类,估计是所有 Java 包里面类最多的,一会是 Input 一会是 Output,一会是 Reader 一会是 Writer,真不知道 Java 的设计者是怎么想的。 + +看着肺都快要气炸的小二,老王深深地吸了一口气,耐心地对小二说:“主要是 Java 的设计者考虑得比较多吧,所以 IO 给人一种很乱的感觉,我来给你梳理一下。” + +### 01、传输方式划分 + +就按照你的那副思维导图来说吧。 + +传输方式有两种,字节和字符,那首先得搞明白字节和字符有什么区别,对吧? + +字节(byte)是计算机中用来表示存储容量的一个计量单位,通常情况下,一个字节有 8 位(bit)。 + +字符(char)可以是计算机中使用的字母、数字、和符号,比如说 A 1 $ 这些。 + +通常来说,一个字母或者一个字符占用一个字节,一个汉字占用两个字节。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/shangtou-02.png) + +具体还要看字符编码,比如说在 UTF-8 编码下,一个英文字母(不分大小写)为一个字节,一个中文汉字为三个字节;在 Unicode 编码中,一个英文字母为一个字节,一个中文汉字为两个字节。 + + PS:关于字符编码,可以看前面的章节:[锟斤拷](https://mp.weixin.qq.com/s/pNQjlXOivIgO3pbYc0GnpA) + +明白了字节与字符的区别,再来看字节流和字符流就会轻松多了。 + +字节流用来处理二进制文件,比如说图片啊、MP3 啊、视频啊。 + +字符流用来处理文本文件,文本文件可以看作是一种特殊的二进制文件,只不过经过了编码,便于人们阅读。 + +换句话说就是,字节流可以处理一切文件,而字符流只能处理文本。 + +虽然 IO 类很多,但核心的就是 4 个抽象类:InputStream、OutputStream、Reader、Writer。 + +(**抽象大法真好**) + +虽然 IO 类的方法也很多,但核心的也就 2 个:read 和 write。 + +**InputStream 类** + +- `int read()`:读取数据 +- `int read(byte b[], int off, int len)`:从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中 +- `long skip(long n)`:跳过指定个数的字节 +- `int available()`:返回可读的字节数 +- `void close()`:关闭流,释放资源 + +**OutputStream 类** + +- `void write(int b)`: 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲) +- `void write(byte b[], int off, int len)`: 将数组 b 中的从 off 位置开始,长度为 len 的字节写入 +- `void flush()`: 强制刷新,将缓冲区的数据写入 +- `void close()`:关闭流 + +**Reader 类** + +- `int read()`:读取单个字符 +- `int read(char cbuf[], int off, int len)`:从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中 +- `long skip(long n)`:跳过指定个数的字符 +- `int ready()`:是否可以读了 +- `void close()`:关闭流,释放资源 + +**Writer 类** + +- `void write(int c)`: 写入一个字符 +- `void write( char cbuf[], int off, int len)`: 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入 +- `void flush()`: 强制刷新,将缓冲区的数据写入 +- `void close()`:关闭流 + +理解了上面这些方法,基本上 IO 的灵魂也就全部掌握了。 + +### 02、操作对象划分 + +小二,你细想一下,IO IO,不就是输入输出(Input/Output)嘛: + +- Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等 +- Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。 + +所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部,比如说文件。 + +文件操作算是 IO 中最典型的操作了,也是最频繁的操作。那其实你可以换个角度来思考,比如说按照 IO 的操作对象来思考,IO 就可以分类为:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/shangtou-03.png) + + +**1)文件** + +文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。 + +FileInputStream 的例子: + +```java +int b; +FileInputStream fis1 = new FileInputStream("fis.txt"); +// 循环读取 +while ((b = fis1.read())!=-1) { + System.out.println((char)b); +} +// 关闭资源 +fis1.close(); +``` + +FileOutputStream 的例子: + +```java +FileOutputStream fos = new FileOutputStream("fos.txt"); +fos.write("沉默王二".getBytes()); +fos.close(); +``` + +FileReader 的例子: + +```java +int b = 0; +FileReader fileReader = new FileReader("read.txt"); +// 循环读取 +while ((b = fileReader.read())!=-1) { + // 自动提升类型提升为 int 类型,所以用 char 强转 + System.out.println((char)b); +} +// 关闭流 +fileReader.close(); +``` + +FileWriter 的例子: + +```java +FileWriter fileWriter = new FileWriter("fw.txt"); +char[] chars = "沉默王二".toCharArray(); +fileWriter.write(chars, 0, chars.length); +fileWriter.close(); +``` + +当掌握了文件的输入输出,其他的自然也就掌握了,都大差不差。 + +**2)数组** + +通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流。 + +ByteArrayInputStream 的例子: + +```java +InputStream is =new BufferedInputStream( + new ByteArrayInputStream( + "沉默王二".getBytes(StandardCharsets.UTF_8))); +//操作 +byte[] flush =new byte[1024]; +int len =0; +while(-1!=(len=is.read(flush))){ + System.out.println(new String(flush,0,len)); +} +//释放资源 +is.close(); +``` + +ByteArrayOutputStream 的例子: + +```java +ByteArrayOutputStream bos =new ByteArrayOutputStream(); +byte[] info ="沉默王二".getBytes(); +bos.write(info, 0, info.length); +//获取数据 +byte[] dest =bos.toByteArray(); +//释放资源 +bos.close(); +``` + +**3)管道** + +Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。 + +一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来。 + +```java +final PipedOutputStream pipedOutputStream = new PipedOutputStream(); +final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream); + +Thread thread1 = new Thread(new Runnable() { + @Override + public void run() { + try { + pipedOutputStream.write("沉默王二".getBytes(StandardCharsets.UTF_8)); + pipedOutputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +}); + +Thread thread2 = new Thread(new Runnable() { + @Override + public void run() { + try { + byte[] flush =new byte[1024]; + int len =0; + while(-1!=(len=pipedInputStream.read(flush))){ + System.out.println(new String(flush,0,len)); + } + + pipedInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + } +}); +thread1.start(); +thread2.start(); +``` + +**4)基本数据类型** + +基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。 + +DataInputStream 提供了一系列可以读基本数据类型的方法: + +```java +DataInputStream dis = new DataInputStream(new FileInputStream(“das.txt”)) ; +byte b = dis.readByte() ; +short s = dis.readShort() ; +int i = dis.readInt(); +long l = dis.readLong() ; +float f = dis.readFloat() ; +double d = dis.readDouble() ; +boolean bb = dis.readBoolean() ; +char ch = dis.readChar() ; +``` + +DataOutputStream 提供了一系列可以写基本数据类型的方法: + +```java +DataOutputStream das = new DataOutputStream(new FileOutputStream(“das.txt”)); +das.writeByte(10); +das.writeShort(100); +das.writeInt(1000); +das.writeLong(10000L); +das.writeFloat(12.34F); +das.writeDouble(12.56); +das.writeBoolean(true); +das.writeChar('A'); +``` + +**5)缓冲** + +CPU 很快,它比内存快 100 倍,比磁盘快百万倍。那也就意味着,程序和内存交互会很快,和硬盘交互相对就很慢,这样就会导致性能问题。 + +为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,比如说 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/io/shangtou-04.png) + + +缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高。 + +**6)打印** + +恐怕 Java 程序员一生当中最常用的就是打印流了:`System.out` 其实返回的就是一个 PrintStream 对象,可以用来打印各式各样的对象。 + +```java +System.out.println("沉默王二是真的二!"); +``` + +PrintStream 最终输出的是字节数据,而 PrintWriter 则是扩展了 Writer 接口,所以它的 `print()/println()` 方法最终输出的是字符数据。使用上几乎和 PrintStream 一模一样。 + +```java +StringWriter buffer = new StringWriter(); +try (PrintWriter pw = new PrintWriter(buffer)) { + pw.println("沉默王二"); +} +System.out.println(buffer.toString()); +``` + +**7)对象序列化/反序列化** + +序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程。 + +```java +ByteArrayOutputStream buffer = new ByteArrayOutputStream(); +try (ObjectOutputStream output = new ObjectOutputStream(buffer)) { + output.writeUTF("沉默王二"); +} +System.out.println(Arrays.toString(buffer.toByteArray())); +``` + +与其对应的,有序列化,就有反序列化,也就是再将字节数组转成 Java 对象的过程。 + +```java +try (ObjectInputStream input = new ObjectInputStream(new FileInputStream( + new File("Person.txt")))) { + String s = input.readUTF(); +} +``` + + +**8)转换** + +InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符。 + +```java +InputStreamReader isr = new InputStreamReader( + new FileInputStream("demo.txt")); +char []cha = new char[1024]; +int len = isr.read(cha); +System.out.println(new String(cha,0,len)); +isr.close(); +``` + +OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁。 + +```java +File f = new File("test.txt") ; +Writer out = new OutputStreamWriter(new FileOutputStream(f)) ; // 字节流变为字符流 +out.write("hello world!!") ; // 使用字符流输出 +out.close() ; +``` + +“小二啊,你看,经过我的梳理,是不是感觉 IO 也没多少东西!针对不同的场景、不同的业务,选择对应的 IO 流就可以了,用法上就是读和写。”老王一口气讲完这些,长长的舒了一口气。 + +此时此刻的小二,还沉浸在老王的滔滔不绝中。不仅感觉老王的肺活量是真的大,还感慨老王不愧是工作了十多年的“老油条”,一下子就把自己感觉头大的 IO 给梳理得很清晰了。 + +--------- + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + + + + + + + + + + diff --git a/docs/src/java8/Lambda.md b/docs/java8/Lambda.md similarity index 85% rename from docs/src/java8/Lambda.md rename to docs/java8/Lambda.md index 30d86efbf7..ee800417f0 100644 --- a/docs/src/java8/Lambda.md +++ b/docs/java8/Lambda.md @@ -1,21 +1,16 @@ --- -title: 深入浅出Java 8 Lambda表达式 -shortTitle: 深入浅出Lambda表达式 category: - Java核心 tag: - - Java新特性 -description: 本文详细介绍了Java 8引入的Lambda表达式,阐述了Lambda表达式的设计目的和用法。通过实际的代码示例,展示了如何使用Lambda表达式来简化代码,提高编程效率。学习本文,让您快速掌握Java 8 Lambda表达式的使用技巧,享受函数式编程带来的编程乐趣。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java8,lambda,java lambda,Lambda表达式, 函数式编程 + - Java --- +# 深入浅出 Java 8 Lambda表达式 + 今天分享的主题是《Lambda 表达式入门》,这也是之前一些读者留言强烈要求我写一写的,不好意思,让你们久等了,现在来满足你们,为时不晚吧? -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-1.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/Lambda-1.jpg) ### 01、初识 Lambda @@ -72,7 +67,7 @@ public class LamadaTest { 是不是很妙!比起匿名内部类,Lambda 表达式不仅易于理解,更大大简化了必须编写的代码数量。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-2.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/Lambda-2.jpg) ### 02、Lambda 语法 @@ -140,8 +135,7 @@ public static void main(String[] args) { 和匿名内部类一样,不要在 Lambda 表达式主体内对方法内的局部变量进行修改,否则编译也不会通过:Lambda 表达式中使用的变量必须是 final 的。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-3.jpg) - +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/Lambda-3.jpg) 这个问题发生的原因是因为 Java 规范中是这样规定的: >Any local variable, formal parameter, or exception parameter used but not declared in a lambda expression @@ -345,7 +339,7 @@ this = com.cmower.java_demo.journal.LamadaTest@3feba861 符合我们分析的预期。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-4.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/Lambda-4.jpg) ### 04、最后 @@ -353,11 +347,4 @@ this = com.cmower.java_demo.journal.LamadaTest@3feba861 好了,我亲爱的读者朋友们,以上就是本文的全部内容了。能在疫情期间坚持看技术文,二哥必须要伸出大拇指为你点个赞👍。原创不易,如果觉得有点用的话,请不要吝啬你手中**点赞**的权力——因为这将是我写作的最强动力。 ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/java8/optional.md b/docs/java8/optional.md similarity index 86% rename from docs/src/java8/optional.md rename to docs/java8/optional.md index 13bdbed986..2672648d5c 100644 --- a/docs/src/java8/optional.md +++ b/docs/java8/optional.md @@ -1,23 +1,19 @@ --- -title: Java 8 Optional最佳指南,优雅解决空指针 -shortTitle: Optional最佳指南 category: - Java核心 tag: - - Java新特性 -description: 本文详细介绍了Java 8引入的Optional类,阐述了Optional的设计初衷和用法。通过实际的代码示例,展示了如何使用Optional来优雅地解决空指针问题,避免程序中的NullPointerException。掌握Optional的使用方法,让您的Java代码更加健壮和可靠。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java8,Optional,java Optional,空指针异常, NullPointerException + - Java +date: 2019-01-01 --- +# Java 8 Optional最佳指南 -想学习,永远都不晚,尤其是针对 Java 8 里面的好东西,Optional 就是其中之一,该类提供了一种用于表示可选值而非空引用的类级别解决方案。作为一名 Java 程序员,我真的是烦透了 [NullPointerException(NPE)](https://javabetter.cn/exception/npe.html),尽管和它熟得就像一位老朋友,知道它也是迫不得已——程序正在使用一个对象却发现这个对象的值为 null,于是 Java 虚拟机就怒发冲冠地把它抛了出来当做替罪羊。 + +想学习,永远都不晚,尤其是针对 Java 8 里面的好东西,Optional 就是其中之一,该类提供了一种用于表示可选值而非空引用的类级别解决方案。作为一名 Java 程序员,我真的是烦透了 NullPointerException(NPE),尽管和它熟得就像一位老朋友,知道它也是迫不得已——程序正在使用一个对象却发现这个对象的值为 null,于是 Java 虚拟机就怒发冲冠地把它抛了出来当做替罪羊。 当然了,我们程序员是富有责任心的,不会坐视不管,于是就有了大量的 null 值检查。尽管有时候这种检查完全没有必要,但我们已经习惯了例行公事。终于,Java 8 看不下去了,就引入了 Optional,以便我们编写的代码不再那么刻薄呆板。 -![](https://cdn.paicoding.com/stutymore/guava-20230329172935.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/optional-1.jpg) ### 01、没有 Optional 会有什么问题 @@ -137,17 +133,17 @@ Optional opt = Optional.of("沉默王二"); System.out.println(opt.isPresent()); // 输出:true Optional optOrNull = Optional.ofNullable(null); -System.out.println(optOrNull.isPresent()); // 输出:false +System.out.println(opt.isPresent()); // 输出:false ``` Java 11 后还可以通过方法 `isEmpty()` 判断与 `isPresent()` 相反的结果。 ```java Optional opt = Optional.of("沉默王二"); -System.out.println(opt.isEmpty()); // 输出:false +System.out.println(opt.isPresent()); // 输出:false Optional optOrNull = Optional.ofNullable(null); -System.out.println(optOrNull.isEmpty()); // 输出:true +System.out.println(opt.isPresent()); // 输出:true ``` ### 05、非空表达式 @@ -179,7 +175,7 @@ opt.ifPresentOrElse(str -> System.out.println(str.length()), () -> System.out.pr 有时候,我们在创建(获取) Optional 对象的时候,需要一个默认值,`orElse()` 和 `orElseGet()` 方法就派上用场了。 -`orElse()` 方法用于返回包裹在 Optional 对象中的值,如果该值不为 null,则返回;否则返回默认值。该方法的参数类型和值的类型一致。 +`orElse()` 方法用于返回包裹在 Optional 对象中的值,如果该值不为 null,则返回;否则返回默认值。该方法的参数类型和值得类型一致。 ```java String nullName = null; @@ -350,15 +346,12 @@ public class OptionalMapFilterDemo { } ``` -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/optional-2.jpg) -好了,我亲爱的读者朋友,以上就是本文的全部内容了——可以说是史上最佳 Optional 指南了,能看到这里的都是最优秀的程序员,二哥必须要伸出大拇指为你点个赞。 ----- -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/optional-2.jpg) -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +好了,我亲爱的读者朋友,以上就是本文的全部内容了——可以说是史上最佳 Optional 指南了,能看到这里的都是最优秀的程序员,二哥必须要伸出大拇指为你点个赞。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/java8/stream.md b/docs/java8/stream.md new file mode 100644 index 0000000000..5f589f7979 --- /dev/null +++ b/docs/java8/stream.md @@ -0,0 +1,317 @@ +--- +category: + - Java核心 +tag: + - Java +date: 2019-01-01 +--- + + + +# Java 8 Stream流详细用法 + + +两个星期以前,就有读者强烈要求我写一篇 Java Stream 流的文章,我说市面上不是已经有很多了吗,结果你猜他怎么说:“就想看你写的啊!”你看你看,多么苍白的喜欢啊。那就“勉为其难”写一篇吧,嘻嘻。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/stream-1.jpg) + +单从“Stream”这个单词上来看,它似乎和 java.io 包下的 InputStream 和 OutputStream 有些关系。实际上呢,没毛关系。Java 8 新增的 Stream 是为了解放程序员操作集合(Collection)时的生产力,之所以能解放,很大一部分原因可以归功于同时出现的 Lambda 表达式——极大的提高了编程效率和程序可读性。 + +Stream 究竟是什么呢? + +>Stream 就好像一个高级的迭代器,但只能遍历一次,就好像一江春水向东流;在流的过程中,对流中的元素执行一些操作,比如“过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等。 + +要想操作流,首先需要有一个数据源,可以是数组或者集合。每次操作都会返回一个新的流对象,方便进行链式操作,但原有的流对象会保持不变。 + +流的操作可以分为两种类型: + +1)中间操作,可以有多个,每次返回一个新的流,可进行链式操作。 + +2)终端操作,只能有一个,每次执行完,这个流也就用光光了,无法执行下一个操作,因此只能放在最后。 + +来举个例子。 + +```java +List list = new ArrayList<>(); +list.add("武汉加油"); +list.add("中国加油"); +list.add("世界加油"); +list.add("世界加油"); + +long count = list.stream().distinct().count(); +System.out.println(count); +``` + +`distinct()` 方法是一个中间操作(去重),它会返回一个新的流(没有共同元素)。 + +```java +Stream distinct(); +``` + +`count()` 方法是一个终端操作,返回流中的元素个数。 + +```java +long count(); +``` + + +中间操作不会立即执行,只有等到终端操作的时候,流才开始真正地遍历,用于映射、过滤等。通俗点说,就是一次遍历执行多个操作,性能就大大提高了。 + +理论部分就扯这么多,下面直接进入实战部分。 + +### 01、创建流 + +如果是数组的话,可以使用 `Arrays.stream()` 或者 `Stream.of()` 创建流;如果是集合的话,可以直接使用 `stream()` 方法创建流,因为该方法已经添加到 Collection 接口中。 + +```java +public class CreateStreamDemo { + public static void main(String[] args) { + String[] arr = new String[]{"武汉加油", "中国加油", "世界加油"}; + Stream stream = Arrays.stream(arr); + + stream = Stream.of("武汉加油", "中国加油", "世界加油"); + + List list = new ArrayList<>(); + list.add("武汉加油"); + list.add("中国加油"); + list.add("世界加油"); + stream = list.stream(); + } +} +``` + +查看 Stream 源码的话,你会发现 `of()` 方法内部其实调用了 `Arrays.stream()` 方法。 + +```java +public static Stream of(T... values) { + return Arrays.stream(values); +} +``` + +另外,集合还可以调用 `parallelStream()` 方法创建并发流,默认使用的是 `ForkJoinPool.commonPool()`线程池。 + +```java +List aList = new ArrayList<>(); +Stream parallelStream = aList.parallelStream(); +``` + +### 02、操作流 + +Stream 类提供了很多有用的操作流的方法,我来挑一些常用的给你介绍一下。 + +1)过滤 + +通过 `filter()` 方法可以从流中筛选出我们想要的元素。 + +```java +public class FilterStreamDemo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("周杰伦"); + list.add("王力宏"); + list.add("陶喆"); + list.add("林俊杰"); + Stream stream = list.stream().filter(element -> element.contains("王")); + stream.forEach(System.out::println); + } +} +``` + +`filter()` 方法接收的是一个 Predicate(Java 8 新增的一个函数式接口,接受一个输入参数返回一个布尔值结果)类型的参数,因此,我们可以直接将一个 Lambda 表达式传递给该方法,比如说 `element -> element.contains("王")` 就是筛选出带有“王”的字符串。 + +`forEach()` 方法接收的是一个 Consumer(Java 8 新增的一个函数式接口,接受一个输入参数并且无返回的操作)类型的参数,`类名 :: 方法名`是 Java 8 引入的新语法,`System.out` 返回 PrintStream 类,println 方法你应该知道是打印的。 + +`stream.forEach(System.out::println);` 相当于在 for 循环中打印,类似于下面的代码: + +```java +for (String s : strs) { + System.out.println(s); +} +``` + +很明显,一行代码看起来更简洁一些。来看一下程序的输出结果: + +``` +王力宏 +``` + +2)映射 + +如果想通过某种操作把一个流中的元素转化成新的流中的元素,可以使用 `map()` 方法。 + +```java +public class MapStreamDemo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("周杰伦"); + list.add("王力宏"); + list.add("陶喆"); + list.add("林俊杰"); + Stream stream = list.stream().map(String::length); + stream.forEach(System.out::println); + } +} +``` + +`map()` 方法接收的是一个 Function(Java 8 新增的一个函数式接口,接受一个输入参数 T,返回一个结果 R)类型的参数,此时参数 为 String 类的 length 方法,也就是把 `Stream` 的流转成一个 `Stream` 的流。 + +程序输出的结果如下所示: + +``` +3 +3 +2 +3 +``` + +3)匹配 + +Stream 类提供了三个方法可供进行元素匹配,它们分别是: + +- `anyMatch()`,只要有一个元素匹配传入的条件,就返回 true。 + +- `allMatch()`,只有有一个元素不匹配传入的条件,就返回 false;如果全部匹配,则返回 true。 + +- `noneMatch()`,只要有一个元素匹配传入的条件,就返回 false;如果全部匹配,则返回 true。 + +```java +public class MatchStreamDemo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("周杰伦"); + list.add("王力宏"); + list.add("陶喆"); + list.add("林俊杰"); + + boolean anyMatchFlag = list.stream().anyMatch(element -> element.contains("王")); + boolean allMatchFlag = list.stream().allMatch(element -> element.length() > 1); + boolean noneMatchFlag = list.stream().noneMatch(element -> element.endsWith("沉")); + System.out.println(anyMatchFlag); + System.out.println(allMatchFlag); + System.out.println(noneMatchFlag); + } +} +``` + +因为“王力宏”以“王”字开头,所以 anyMatchFlag 应该为 true;因为“周杰伦”、“王力宏”、“陶喆”、“林俊杰”的字符串长度都大于 1,所以 allMatchFlag 为 true;因为 4 个字符串结尾都不是“沉”,所以 noneMatchFlag 为 true。 + +程序输出的结果如下所示: + +``` +true +true +true +``` + +4)组合 + +`reduce()` 方法的主要作用是把 Stream 中的元素组合起来,它有两种用法: + +- `Optional reduce(BinaryOperator accumulator)` + +没有起始值,只有一个参数,就是运算规则,此时返回 [Optional](https://mp.weixin.qq.com/s/PqK0KNVHyoEtZDtp5odocA)。 + +- `T reduce(T identity, BinaryOperator accumulator)` + +有起始值,有运算规则,两个参数,此时返回的类型和起始值类型一致。 + +来看下面这个例子。 + +```java +public class ReduceStreamDemo { + public static void main(String[] args) { + Integer[] ints = {0, 1, 2, 3}; + List list = Arrays.asList(ints); + + Optional optional = list.stream().reduce((a, b) -> a + b); + Optional optional1 = list.stream().reduce(Integer::sum); + System.out.println(optional.orElse(0)); + System.out.println(optional1.orElse(0)); + + int reduce = list.stream().reduce(6, (a, b) -> a + b); + System.out.println(reduce); + int reduce1 = list.stream().reduce(6, Integer::sum); + System.out.println(reduce1); + } +} +``` + +运算规则可以是 [Lambda 表达式](https://mp.weixin.qq.com/s/ozr0jYHIc12WSTmmd_vEjw)(比如 `(a, b) -> a + b`),也可以是类名::方法名(比如 `Integer::sum`)。 + +程序运行的结果如下所示: + +```java +6 +6 +12 +12 +``` + +0、1、2、3 在没有起始值相加的时候结果为 6;有起始值 6 的时候结果为 12。 + +### 03、转换流 + +既然可以把集合或者数组转成流,那么也应该有对应的方法,将流转换回去——`collect()` 方法就满足了这种需求。 + +```java +public class CollectStreamDemo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("周杰伦"); + list.add("王力宏"); + list.add("陶喆"); + list.add("林俊杰"); + + String[] strArray = list.stream().toArray(String[]::new); + System.out.println(Arrays.toString(strArray)); + + List list1 = list.stream().map(String::length).collect(Collectors.toList()); + List list2 = list.stream().collect(Collectors.toCollection(ArrayList::new)); + System.out.println(list1); + System.out.println(list2); + + String str = list.stream().collect(Collectors.joining(", ")).toString(); + System.out.println(str); + } +} +``` + +`toArray()` 方法可以将流转换成数组,你可能比较好奇的是 `String[]::new`,它是什么东东呢?来看一下 `toArray()` 方法的源码。 + +```java + A[] toArray(IntFunction generator); +``` + +也就是说 `String[]::new` 是一个 IntFunction,一个可以产生所需的新数组的函数,可以通过反编译字节码看看它到底是什么: + +```java +String[] strArray = (String[])list.stream().toArray((x$0) -> { + return new String[x$0]; +}); +System.out.println(Arrays.toString(strArray)); +``` + +也就是相当于返回了一个指定长度的字符串数组。 + +当我们需要把一个集合按照某种规则转成另外一个集合的时候,就可以配套使用 `map()` 方法和 `collect()` 方法。 + +```java +List list1 = list.stream().map(String::length).collect(Collectors.toList()); +``` + +通过 `stream()` 方法创建集合的流后,再通过 `map(String:length)` 将其映射为字符串长度的一个新流,最后通过 `collect()` 方法将其转换成新的集合。 + +Collectors 是一个收集器的工具类,内置了一系列收集器实现,比如说 `toList()` 方法将元素收集到一个新的 `java.util.List` 中;比如说 `toCollection()` 方法将元素收集到一个新的 ` java.util.ArrayList` 中;比如说 `joining()` 方法将元素收集到一个可以用分隔符指定的字符串中。 + +来看一下程序的输出结果: + +```java +[周杰伦, 王力宏, 陶喆, 林俊杰] +[3, 3, 2, 3] +[周杰伦, 王力宏, 陶喆, 林俊杰] +周杰伦, 王力宏, 陶喆, 林俊杰 +``` + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/java8/stream-2.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/src/jvm/asm.md b/docs/jvm/asm.md similarity index 88% rename from docs/src/jvm/asm.md rename to docs/jvm/asm.md index f722e6ae90..97822b78dc 100644 --- a/docs/src/jvm/asm.md +++ b/docs/jvm/asm.md @@ -1,17 +1,12 @@ --- -title: 史上最通俗易懂的ASM教程 -shortTitle: 史上最通俗易懂的ASM教程 category: - Java核心 + - JVM tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,史上最通俗易懂的ASM教程 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,asm + - Java --- +# 史上最通俗易懂的ASM教程 ## 一勺思想 @@ -32,7 +27,7 @@ ASM是一种通用Java字节码操作和分析框架。它可以用于修改现 我们编写的java文件,会通过javac命令编译为class文件,JVM最终会执行该类型文件来运行程序。下图所示为class文件结构。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-43844b78-c01f-4990-b038-3c91ff2eeb34.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/asm-43844b78-c01f-4990-b038-3c91ff2eeb34.jpg) 下面我们通过一个简单的实例来进行说明。下面是我们编写的一个简单的java文件,只是简单的函数调用. @@ -191,7 +186,7 @@ SourceFile: "Test.java" JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性。在线程中执行一个方法时,我们会创建一个栈帧入栈并执行,如果该方法又调用另一个方法时会再次创建新的栈帧然后入栈,方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧,随后虚拟机将会丢弃此栈帧。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-e31b7e50-1d48-4eef-9552-6fa7e6c68fed.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/asm-e31b7e50-1d48-4eef-9552-6fa7e6c68fed.jpg) ### 局部变量表 @@ -243,9 +238,9 @@ public int sub(int, int); a = b + c 的字节码执行过程中操作数栈以及局部变量表的变化如下图所示 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-4670450e-6199-4562-9cf4-354234c734c8.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/asm-4670450e-6199-4562-9cf4-354234c734c8.jpg) -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-9808d639-327f-4796-80d4-1809be0b9106.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/asm-9808d639-327f-4796-80d4-1809be0b9106.jpg) ## ASM操作 @@ -346,17 +341,10 @@ mv.visitEnd(); 可以一键生成对应的ASM API代码 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-3c8c8db4-5b6a-4576-b147-62965d0e0c1c.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/asm-3c8c8db4-5b6a-4576-b147-62965d0e0c1c.jpg) ---- >参考链接:https://zhuanlan.zhihu.com/p/94498015 ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/bytecode.md b/docs/jvm/bytecode.md new file mode 100644 index 0000000000..72d40ca5ea --- /dev/null +++ b/docs/jvm/bytecode.md @@ -0,0 +1,456 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# 从javap的角度轻松看懂字节码 + + +### 01、字节码 + +计算机比较“傻”,只认 0 和 1,这意味着我们编写的代码最终都要编译成机器码才能被计算机执行。Java 在诞生之初就提出了一个非常著名的宣传口号: "**一次编写,处处运行**"。 + +> **Write Once, Run Anywhere.** + +为了这个口号,Java 的亲妈 Sun 公司以及其他虚拟机提供商发布了许多可以在不同平台上运行的 Java 虚拟机,而这些虚拟机都拥有一个共同的功能,那就是可以载入和执行同一种与平台无关的字节码(Byte Code)。 + +有了 Java 虚拟机的帮助,我们编写的 Java 源代码不必再根据不同平台编译成对应的机器码了,只需要生成一份字节码,然后再将字节码文件交由运行在不同平台上的 Java 虚拟机读取后执行就可以了。 + +如今的 Java 虚拟机非常强大,不仅支持 Java 语言,还支持很多其他的编程语言,比如说 Groovy、Scala、Koltin 等等。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-dd31bbd6-c75c-4426-9437-c0f57ea3b86f.png) + +来看一段代码吧。 + +```java +public class Main { + private int age = 18; + public int getAge() { + return age; + } +} +``` + +编译生成 Main.class 文件后,可以在命令行使用 `xxd Main.class` 打开 class 文件(我用的是 Intellij IDEA,在 macOS 环境下)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-bd941085-ff0e-4abf-a5f9-afb0493bfed7.png) + + + +对于这些 16 进制内容,除了开头的 cafe babe,剩下的内容大致可以翻译成:啥玩意啊这...... + +同学们别慌,就从"cafe babe"说起吧,这 4 个字节称之为**魔数**,也就是说,只有以"cafe babe"开头的 class 文件才能被 Java 虚拟机接受,这 4 个字节就是字节码文件的身份标识。 + +目光右移,0000 是 Java 的次版本号,0037 转化为十进制是 55,是主版本号,Java 的版本号从 45 开始,每升一个大版本,版本号加 1,大家可以启动福尔摩斯模式,推理一下。 + +再往后面就是字符串常量池。《[class 文件](https://mp.weixin.qq.com/s/uMEZ2Xwctx4n-_8zvtDp5A)》那一篇我是顺着十六进制内容往下分析的,可能初学者看起来比较头大,这次我们换一种更容易懂的方式。 + +### **02、反编译字节码文件** + +Java 内置了一个反编译命令 javap,可以通过 `javap -help` 了解 javap 的基本用法。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-84b7af5c-93b1-4f63-bb30-946ab3d7e98c.png) + + +OK,我们输入命令 `javap -v -p Main.class` 来查看一下输出的内容。 + +``` +Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class + Last modified 2021年4月15日; size 385 bytes + SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c6 + Compiled from "Main.java" +public class com.itwanger.jvm.Main + minor version: 0 + major version: 55 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #3 // com/itwanger/jvm/Main + super_class: #4 // java/lang/Object + interfaces: 0, fields: 1, methods: 2, attributes: 1 +Constant pool: + #1 = Methodref #4.#18 // java/lang/Object."":()V + #2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I + #3 = Class #20 // com/itwanger/jvm/Main + #4 = Class #21 // java/lang/Object + #5 = Utf8 age + #6 = Utf8 I + #7 = Utf8 + #8 = Utf8 ()V + #9 = Utf8 Code + #10 = Utf8 LineNumberTable + #11 = Utf8 LocalVariableTable + #12 = Utf8 this + #13 = Utf8 Lcom/itwanger/jvm/Main; + #14 = Utf8 getAge + #15 = Utf8 ()I + #16 = Utf8 SourceFile + #17 = Utf8 Main.java + #18 = NameAndType #7:#8 // "":()V + #19 = NameAndType #5:#6 // age:I + #20 = Utf8 com/itwanger/jvm/Main + #21 = Utf8 java/lang/Object +{ + private int age; + descriptor: I + flags: (0x0002) ACC_PRIVATE + + public com.itwanger.jvm.Main(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=2, locals=1, args_size=1 + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."":()V + 4: aload_0 + 5: bipush 18 + 7: putfield #2 // Field age:I + 10: return + LineNumberTable: + line 6: 0 + line 7: 4 + LocalVariableTable: + Start Length Slot Name Signature + 0 11 0 this Lcom/itwanger/jvm/Main; + + public int getAge(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=1, args_size=1 + 0: aload_0 + 1: getfield #2 // Field age:I + 4: ireturn + LineNumberTable: + line 9: 0 + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 this Lcom/itwanger/jvm/Main; +} +SourceFile: "Main.java" +``` + +睁大眼睛瞧过去,感觉内容挺多的。同学们不要着急,我们来一行一行分析。 + +第 1 行: + +``` +Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class +``` + +字节码文件的位置。 + +第 2 行: + +``` +Last modified 2021年4月15日; size 385 bytes +``` + +字节码文件的修改日期、文件大小。 + +第 3 行: + +``` +SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c +``` + +字节码文件的 SHA-256 值。 + +第 4 行: + +``` +Compiled from "Main.java" +``` + + 说明该字节码文件编译自 Main.java 源文件。 + +第 5 行: + +``` +public class com.itwanger.jvm.Main +``` + +字节码文件的类全名。 + +第 6 行 `minor version: 0`,次版本号。 + +第 7 行 `major version: 55`,主版本号。 + +第 8 行: + +``` +flags: (0x0021) ACC_PUBLIC, ACC_SUPER +``` + +类访问标记,一共有 8 种。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-d12d6983-f427-40d2-bb4b-3a2c6c4c7806.png) + +表明当前类是 `ACC_PUBLIC | ACC_SUPER`。位运算符 `|` 的意思是如果相对应位是 0,则结果为 0,否则为 1,所以 `0x0001 | 0x0020` 的结果是 `0x0021`(需要转成二进制进行运算)。 + +第 9 行: + +``` +this_class: #3 // com/itwanger/jvm/Main +``` + +当前类的索引,指向常量池中下标为 3 的常量,可以看得出当前类是 Main 类。 + +第 10 行: + +``` +super_class: #4 // java/lang/Object +``` + +父类的索引,指向常量池中下标为 6 的常量,可以看得出当前类的父类是 Object 类。 + +第 11 行: + +``` +interfaces: 0, fields: 1, methods: 2, attributes: 1 +``` + +当前类有 0 个接口,1 个字段(age),2 个方法(`write()`方法和缺省的默认构造方法),1 个属性(该类仅有的一个属性是 SourceFIle,包含了源码文件的信息)。 + +### 03、常量池 + +接下来是 Constant pool,也就是字节码文件最重要的常量池部分。可以把常量池理解为字节码文件中的资源仓库,主要存放两大类信息。 + +1)字面量(Literal),有点类似 Java 中的常量概念,比如文本字符串,final 常量等。 + +2)符号引用(Symbolic References),属于编译原理方面的概念,包括 3 种: + +- 类和接口的全限定名(Fully Qualified Name) +- 字段的名称和描述符(Descriptor) +- 方法的名称和描述符 + +Java 虚拟机是在加载字节码文件的时候才进行的动态链接,也就是说,字段和方法的符号引用只有经过运行期转换后才能获得真正的内存地址。当 Java 虚拟机运行时,需要从常量池获取对应的符号引用,然后在类创建或者运行时解析并翻译到具体的内存地址上。 + +当前字节码文件中一共有 21 个常量,它们之间是有链接的,逐个分析会比较乱,我们采用顺藤摸瓜的方式,从上依次往下看,那些被链接的常量我们就点到为止。 + +*注*: + +- `#` 号后面跟的是索引,索引没有从 0 开始而是从 1 开始,是因为设计者考虑到,“如果要表达不引用任何一个常量的含义时,可以将索引值设为 0 来表示”(《深入理解 Java 虚拟机》描述的)。 + +- `=` 号后面跟的是常量的类型,没有包含前缀 `CONSTANT_` 和后缀 `_info`。 + +- 全文中提到的索引等同于下标,为了灵活描述,没有做统一。 + +--- + +第 1 个常量: + +``` +#1 = Methodref #4.#18 // java/lang/Object."":()V +``` + +类型为 Methodref,表明是用来定义方法的,指向常量池中下标为 4 和 18 的常量。 + +第 4 个常量: + +``` +#4 = Class #21 // java/lang/Object +``` + +类型为 Class,表明是用来定义类(或者接口)的,指向常量池中下标为 21 的常量。 + +第 21 个常量: + +``` +#21 = Utf8 java/lang/Object +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 `java/lang/Object`。 + +第 18 个常量: + +``` +#18 = NameAndType #7:#8 // "":()V +``` + +类型为 NameAndType,表明是字段或者方法的部分符号引用,指向常量池中下标为 7 和 8 的常量。 + +第 7 个常量: + +``` +#7 = Utf8 +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 ``,表明为构造方法。 + +第 8 个常量: + +``` +#8 = Utf8 ()V +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 `()V`,表明方法的返回值为 void。 + +到此为止,第 1 个常量算是摸完了。组合起来的意思就是,Main 类使用的是默认的构造方法,来源于 Object 类。 + +---- + +第 2 个常量: + +``` +#2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I +``` + +类型为 Fieldref,表明是用来定义字段的,指向常量池中下标为 3 和 19 的常量。 + +第 3 个常量: + +``` +#3 = Class #20 // com/itwanger/jvm/Main +``` + +类型为 Class,表明是用来定义类(或者接口)的,指向常量池中下标为 20 的常量。 + +第 19 个常量: + +``` +#19 = NameAndType #5:#6 // age:I +``` + +类型为 NameAndType,表明是字段或者方法的部分符号引用,指向常量池中下标为 5 和 6 的常量。 + +第 5 个常量: + +``` +#5 = Utf8 age +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 `age`,表明字段名为 age。 + +第 6 个常量: + +``` +#6 = Utf8 I +``` + +类型为 Utf8,UTF-8 编码的字符串,值为 `I`,表明字段的类型为 int。 + +关于字段类型的描述符映射表如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-cbf16ce9-7853-4050-a1c0-8b874f3b0c1e.png) + +到此为止,第 2 个常量算是摸完了。组合起来的意思就是,声明了一个类型为 int 的字段 age。 + +---- + +### 04、字段表集合 + +字段表用来描述接口或者类中声明的变量,包括类变量和成员变量,但不包含声明在方法中局部变量。 + +字段的修饰符一般有: + +- 访问权限修饰符,比如 public private protected +- 静态变量修饰符,比如 static +- final 修饰符 +- 并发可见性修饰符,比如 volatile +- 序列化修饰符,比如 transient + +然后是字段的类型(可以是基本数据类型、数组和对象)和名称。 + +在 Main.class 字节码文件中,字段表的信息如下所示。 + +``` +private int age; + descriptor: I + flags: (0x0002) ACC_PRIVATE +``` + +表明字段的访问权限修饰符为 private,类型为 int,名称为 age。 + +字段的访问标志和类的访问标志非常类似。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-5f328e11-3486-4eb4-8fa9-5c5febfab894.png) + + + + +### **05、方法表集合** + +方法表用来描述接口或者类中声明的方法,包括类方法和成员方法,以及构造方法。方法的修饰符和字段略有不同,比如说 volatile 和 transient 不能用来修饰方法,再比如说方法的修饰符多了 synchronized、native、strictfp 和 abstract。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/bytecode-fd434d5c-ffc6-4a24-9787-98e573035068.png) + +下面这部分为构造方法,返回类型为 void,访问标志为 public。 + +``` + public com.itwanger.jvm.Main(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC +``` + +来详细看一下其中 Code 属性。 + +``` + Code: + stack=2, locals=1, args_size=1 + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."":()V + 4: aload_0 + 5: bipush 18 + 7: putfield #2 // Field age:I + 10: return + LineNumberTable: + line 6: 0 + line 7: 4 + LocalVariableTable: + Start Length Slot Name Signature + 0 11 0 this Lcom/itwanger/jvm/Main; +``` + +- stack 为最大操作数栈,Java 虚拟机在运行的时候会根据这个值来分配栈帧的操作数栈深度。 + +- locals 为局部变量所需要的存储空间,单位为槽(slot),方法的参数变量和方法内的局部变量都会存储在局部变量表中。 + +- args_size 为方法的参数个数。 + +为什么 stack 的值为 2,locals 的值为 1,args_size 的值为 1 呢? 默认的构造方法不是没有参数和局部变量吗? + +这是因为有一个隐藏的 this 变量,只要不是静态方法,都会有一个当前类的对象 this 悄悄的存在着。这就解释了为什么 locals 和 args_size 的值为 1 的问题。那为什么 stack 的值为 2 呢?因为字节码指令 invokespecial(调用父类的构造方法进行初始化)会消耗掉一个当前类的引用,所以 aload_0 执行了 2 次,也就意味着操作数栈的大小为 2。 + +关于字节码指令,我们后面再详细介绍。 + +- LineNumberTable,该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。 + +- LocalVariableTable,该属性的作用是描述帧栈中的局部变量与源码中定义的变量之间的关系。大家仔细看一下,就能看到 this 的影子了。 + +下面这部分为成员方法 `getAge()`,返回类型为 int,访问标志为 public。 + +``` + public int getAge(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC +``` + +理解了构造方法的 Code 属性后,再看 `getAge()` 方法的 Code 属性时,就很容易理解了。 + +``` + Code: + stack=1, locals=1, args_size=1 + 0: aload_0 + 1: getfield #2 // Field age:I + 4: ireturn + LineNumberTable: + line 9: 0 + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 this Lcom/itwanger/jvm/Main; +``` + +最大操作数栈为 1,局部变量所需要的存储空间为 1,方法的参数个数为 1,是因为局部变量只有一个隐藏的 this,并且字节码指令中只执行了一次 aload_0。 + +------- + + +其实学习是这样的,可以横向扩展,也可以纵向扩展。当我们初学编程的时候,特别想多学一点,属于横向扩展,当有了一定的编程经验后,想更上一层楼,就需要纵向扩展,不断深入地学,连根拔起,从而形成自己的知识体系。 + +无论是从十六进制的字节码角度,还是 jclasslib 图形化查看反编译后的字节码的角度,也或者是今天这样从 javap 反编译后的角度,都能窥探出一些新的内容来! + +初学者一开始接触字节码的时候会感觉比较头大,没关系,我当初也是这样,随着时间的推移,经验的积累,慢慢就好了,越往深处钻,就越能体会到那种“技术我有,雄霸天下”的感觉~ + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/class-file-jiegou.md b/docs/jvm/class-file-jiegou.md new file mode 100644 index 0000000000..c65ab15459 --- /dev/null +++ b/docs/jvm/class-file-jiegou.md @@ -0,0 +1,363 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# 详解Java的类文件(class文件)结构 + + +大家好,我是二哥呀,今天我拿了一把小刀,准备解剖一下 Java 的 class 文件。 + +CS 的世界里流行着这么一句话,“计算机科学领域的任何问题都可以通过增加一个中间层来解决”。对于 Java 来说,JVM 就是这么一个产物,“Write once, Run anywhere”之所以能实现,靠得就是 JVM,它能在不同的操作系统下运行同一份源代码编译后的 class 文件。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-dfd7ce0d-1da2-4547-b2d7-57e0350f5911.png) + +Java 是跨平台的,JVM 作为中间层,自然要针对不同的操作系统提供不同的实现。拿 JDK 11 来说,它的实现就有上图中提到的这么多种。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-b1386f9e-c69b-44b0-a8d0-69ffbe9ed31f.png) + +通过不同操作系统的 JVM,我们的源代码就可以不用根据不同的操作系统编译成不同的二进制可执行文件了,跨平台的目标也就实现了。那这个 class 文件到底是什么玩意呢?它是怎么被 JVM 识别的呢? + +我们用 IDEA 编写一段简单的 Java 代码,文件名为 Hello.java。 + +```java +package com.itwanger.jvm; +class Hello { + public static void main(String[] args) { + System.out.println("Hello!"); + } +} +``` + +点击编译按钮后,IDEA 会帮我们自动生成一个名为 Hello.class 的文件,在 `target/classes` 的对应包目录下。直接双击打开后长下面这样子: + +```java +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.itwanger.jvm; + +class Hello { + Hello() { + } + + public static void main(String[] args) { + System.out.println("Hello!"); + } +} +``` + +看起来和源代码很像,只是多了一个空的构造方法,对吧?它是 class 文件被 IDEA 自带的反编译工具 Fernflower 反编译后的样子。那真实的 class 文件长什么样子呢? + +可以在 terminal 面板下用 `xxd Hello.class` 命令来查看。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-cb4afe63-6a8e-4ae1-a822-d4163c814daa.png) + +咦?完全看不懂的样子呢。它是 class 文件的一种十六进制形式,`xxd` 这个命令的神奇之处就是它能将一个给定文件转换成十六进制形式。 + +### 01、魔数 + +第一行中有一串特殊的字符 `cafebabe`,它就是一个魔数,是 JVM 识别 class 文件的标志,JVM 会在验证阶段检查 class 文件是否以该魔数开头,如果不是则会抛出 `ClassFormatError`。 + +魔数 `cafebabe` 的中文意思显而易见,咖啡宝贝,再加上 Java 的图标本来就是一个热气腾腾的咖啡,可见 Java 与咖啡的渊源有多深。 + +### 02、版本号 + +紧跟着魔数后面的四个字节 `0000 0037` 分别表示副版本号和主版本号。也就是说,主版本号为 55(0x37 的十进制),也就是 Java 11 对应的版本号,副版本号为 0。 + +上一个 LTS 版本是 Java 8,对应的主版本号为 52,也就是说 Java 9 是 53,Java 10 是 54,只不过 Java 9 和 Java 10 都是过渡版本,下一个 LTS 版本是 Java 17,预计 2021 年 9 月份推出。 + +### 03、常量池 + +紧跟在版本号之后的是常量池,字符串常量和较大的证书都会存储在常量池中,当使用这些数值时,会根据常量池中的索引来查找。 + +Java 定义了 boolean、byte、short、char 和 int 等基本数据类型,它们在常量池中都会被当做 int 来处理。我们来通过一段简单的 Java 代码了解下。 + +```java +public class ConstantTest { + public final boolean bool = true; + public final char aChar = 'a'; + public final byte b = 66; + public final short s = 67; + public final int i = 68; +} +``` + +布尔值 true 的十六进制是 0x01、字符 a 的十六进制是 0x61,字节 66 的十六进制是 0x42,短整型 67 的十六进制是 0x43,整型 68 的十六进制是 0x44。所以编译生成的整型常量在 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-bbe4c673-c3a5-4952-901d-35446f91a3af.png) + +第一个字节 0x03 表示常量的类型为 *CONSTANT_Integer_info*,是 JVM 中定义的 14 种常量类型之一,对应的还有 *CONSTANT_Float_info*、*CONSTANT_Long_info*、*CONSTANT_Double_info*,对应的标识分别是 0x04、0x05、0x06。 + +对于 int 和 float 来说,它们占 4 个字节;对于 long 和 double 来说,它们占 8 个字节。来个 long 型的最大值观察下。 + +```java +public class ConstantTest { + public final long ong = Long.MAX_VALUE; +} +``` + +来看一下它在 class 文件中的位置。05 开头,7f ff ff ff ff ff ff ff 结尾,果然占 8 个字节,以前知道 long 型会占 8 个字节,但没有直观的感受,现在有了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-2c876f52-1cc1-4076-807a-d85a1cb80e75.png) + +接下来,我们再来看一段代码。 + +```java +class Hello { + public final String s = "hello"; +} +``` + +“hello”是一个字符串,它的十六进制为 `68 65 6c 6c 6f`,我们来看一下它在 class 文件中的位置。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-801ed589-658c-407e-ac64-81fd525d7324.png) + +前面还有 3 个字节,第一个字节 0x01 是标识,标识类型为 *CONSTANT_Uft8_info*,第二个和第三个自己 0x00 0x05 用来表示第三部分字节数组的长度。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-ae4f38c9-68fe-40ad-91c6-3e7fd360de05.png) + +与 *CONSTANT_Uft8_info* 类型对应的,还有一个 *CONSTANT_String_info*,用来表示字符串对象(之前代码中的 s),标识是 0x08。前者存储了字符串真正的值,后者并不包含字符串的内容,仅仅包含了一个指向常量池中 *CONSTANT_Uft8_info* 的索引。来看一下它在 class 文件中的位置。 + + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-4e093bef-d592-4be7-847e-0ef5900c5fa4.png) + +*CONSTANT_String_info* 通过索引 19 来找到 *CONSTANT_Uft8_info*。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-85af064d-5dc6-4187-b4f3-3501ccfc99b3.png) + +除此之外,还有 *CONSTANT_Class_info*,用来表示类和接口,结构和 *CONSTANT_String_info* 类似,第一个字节是标识,值为 0x07,后面两个字节是常量池索引,指向 *CONSTANT_Utf8_info*——字符串存储的是类或者接口的全路径限定名。 + +拿 Hello.java 类来说,它的全路径限定名为 `com/itwanger/jvm/Hello`,对应的十六进制为“636f6d2f697477616e6765722f6a766d2f48656c6c6f”,是一串 *CONSTANT_Uft8_info*,指向它的 *CONSTANT_Class_info* 在 class 文件中的什么位置呢? + +先不着急,这里给大家介绍一款可视化字节码的工具 jclasslib bytecode viewer,可以直接在 IDEA 的插件市场安装。安装完成后,选中 class 文件,然后在 View 菜单里找到 Show Bytecode With Jclasslib 子菜单,就可以查看 class 文件的关键信息了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-ac6cc8cf-ed25-4bbb-8685-d473ecf15a60.png) + +从上图中可以看到,常量池的总大小为 23,索引为 04 的 *CONSTANT_Class_info* 指向的是是索引为 21 的 *CONSTANT_Uft8_info*,值为 `com/itwanger/jvm/Hello`。21 的十六进制为 0x15,有了这个信息,我们就可以找到 *CONSTANT_Class_info* 在 class 文件中的位置了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-74816960-b8f7-42f3-9001-c05ebd25f58d.png) + +0x07 是第一个字节,*CONSTANT_Class_info* 的标识符,然后是两个字节,标识索引。 + +还有 *CONSTANT_NameAndType_info*,用来标识字段或方法,标识符为 12,对应的十六进制是 0x0c。后面还有 4 个字节,前两个是字段或者方法的索引,后两个是字段或方法的描述符,也就是字段或者方法的类型。 + +来看下面这段代码。 + +```java +class Hello { + public void testMethod(int id, String name) { + } +} +``` + +用 jclasslib 可以看到 *CONSTANT_NameAndType_info* 包含的索引有两个。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-70cd8902-136c-42a4-ab57-d6baf202e462.png) + +一个是 4,一个是 5,可以通过下图来表示 *CONSTANT_NameAndType_info* 的构成。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-5ac7d4c4-b905-462c-90f7-58b46fc5dda1.png) + +对应 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-eba9e047-d6fb-43e7-8c27-3683a076ccdd.png) + +接下来是 *CONSTANT_Fieldref_info* 、*CONSTANT_Methodref_info* 和 *CONSTANT_InterfaceMethodref_info*,它们三个的结构比较类似,可以通过下面的伪代码来表示。 + +``` +CONSTANT_*ref_info { + u1 tag; + u2 class_index; + u2 name_and_type_index; +} +``` + +学过 C 语言的符号表(Symbol Table)的话,对这段伪代码并不会陌生。 + +- tag 为标识符,Fieldref 的为 9,也就是十六进制的 0x09;Methodref 的为 10,也就是十六进制的 0x0a;InterfaceMethodref 的为 11, 也就是十六进制的 0x0b。 +- class_index 为 *CONSTANT_Class_info* 的常量池索引,表示字段 | 方法 | 接口方法所在的类信息。 +- name_and_type_index 为 *CONSTANT_NameAndType_info* 的常量池索引,拿 Fieldref 来说,表示字段名和字段类型;拿 Methodref 来说,表示方法名、方法的参数和返回值类型;拿 InterfaceMethodref 来说,表示接口方法名、接口方法的参数和返回值类型。 + +还有 *CONSTANT_MethodHandle_info* 、*CONSTANT_MethodType_info* 和 *CONSTANT_InvokeDynamic_info*,我就不再一一说明了,大家也可以拿把小刀去试一试。 + +啊,class 文件中最复杂的常量池部分就算是解剖完了,真不容易! + +### 04、访问标记 + +紧跟着常量池之后的区域就是访问标记(Access flags),这个标记用于识别类或接口的访问信息,比如说到底是 class 还是 interface?是 public 吗?是 abstract 抽象类吗?是 final 类吗?等等。总共有 16 个标记位可供使用,但常用的只有其中 7 个。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-1f5d3154-9a28-4cfa-935e-43d7e023036e.png) + +来看一个简单的枚举代码。 + +```java +public enum Color { + RED,GREEN,BLUE; +} +``` + +通过 jclasslib 可以看到访问标记的信息有 `0x4031 [public final enum]`。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-d4873db5-1a9d-4e05-9765-59a71b083fe5.png) + +对应 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-774e8289-582b-4762-9dce-b0590ee5ad3f.png) + +### 05、this_class、super_class、interfaces + +这三部分用来确定类的继承关系,this_class 为当前类的索引,super_class 为父类的索引,interfaces 为接口。 + +来看下面这段简单的代码,没有接口,默认继承 Object 类。 + +```java +class Hello { + public static void main(String[] args) { + + } +} +``` + +通过 jclasslib 可以看到类的继承关系。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-77c4ecff-6d36-405d-93da-ee06431bf312.png) + +- this_class 指向常量池中索引为 2 的 *CONSTANT_Class_info*。 +- super_class 指向常量池中索引为 3 的 *CONSTANT_Class_info*。 +- 由于没有接口,所以 interfaces 的信息为空。 + +对应 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-6d5d9189-12a2-45d3-b811-92deede2f78d.png) + +### 06、字段表 + +一个类中定义的字段会被存储在字段表(fields)中,包括静态的和非静态的。 + +来看这样一段代码。 + +```java +public class FieldsTest { + private String name; +} +``` + +字段只有一个,修饰符为 private,类型为 String,字段名为 name。可以用下面的伪代码来表示 field 的结构。 + +``` +field_info { + u2 access_flag; + u2 name_index; + u2 description_index; +} +``` + +- access_flag 为字段的访问标记,比如说是不是 public | private | protected,是不是 static,是不是 final 等。 +- name_index 为字段名的索引,指向常量池中的 *CONSTANT_Utf8_info*, 比如说上例中的值就为 name。 +- description_index 为字段的描述类型索引,也指向常量池中的 *CONSTANT_Utf8_info*,针对不同的数据类型,会有不同规则的描述信息。 + +1)对于基本数据类型来说,使用一个字符来表示,比如说 I 对应的是 int,B 对应的是 byte。 + +2)对于引用数据类型来说,使用 `L***;` 的方式来表示,`L` 开头,`;` 结束,比如字符串类型为 `Ljava/lang/String;`。 + +3)对于数组来说,会用一个前置的 `[` 来表示,比如说字符串数组为 `[Ljava/lang/String;`。 + +对应到 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-5a40ed62-4ff2-4101-b2d5-15760032f563.png) + +### 07、方法表 + +方法表和字段表类似,区别是用来存储方法的信息,包括方法名,方法的参数,方法的签名。 + +就拿 main 方法来说吧。 + +```java +public class MethodsTest { + public static void main(String[] args) { + + } +} +``` + +先用 jclasslib 看一下大概的信息。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-cbe6d025-84a5-4fea-821b-a4234f47c6cd.png) + +- 访问标记是 public static 的。 +- 方法名为 main。 +- 方法的参数为字符串数组;返回类型为 Void。 + +对应到 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-f3932093-46f3-4ef0-8598-bbd70515a9bd.png) + +### 08、属性表 + +属性表是 class 文件中的最后一部分,通常出现在字段和方法中。 + +来看这样一段代码。 + +```java +public class AttributeTest { + public static final int DEFAULT_SIZE = 128; +} +``` + +只有一个常量 DEFAULT_SIZE,它属于字段中的一种,就是加了 final 的静态变量。先通过 jclasslib 看一下它当中一个很重要的属性——ConstantValue,用来表示静态变量的初始值。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-dee995d6-e285-4a31-b11f-e93c3599cd8e.png) + + +- Attribute name index 指向常量池中值为“ConstantValue”的常量。 +- Attribute length 的值为固定的 2,因为索引只占两个字节的大小。 +- Constant value index 指向常量池中具体的常量,如果常量类型为 int,指向的就是 *CONSTANT_Integer_info*。 + +我画了一副图,可以完整的表示字段的结构,包含属性表在内。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-53f73e24-f060-45d2-8e29-34263c31847b.png) + +对应到 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-423341a7-3aeb-4ac9-95e8-a1e7f7847255.png) + +来看下面这段代码。 + +```java +public class MethodCode { + public static void main(String[] args) { + foo(); + } + + private static void foo() { + } +} +``` + +main 方法中调用了 foo 方法。通过 jclasslib 看一下它当中一个很重要的属性——Code, 方法的关键信息都存储在里面。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-e76339a8-0aab-418b-9722-4b3c8591693c.png) + + +- Attribute name index 指向常量池中值为“Code”的常量。 +- Attribute length 为属性值的长度大小。 +- bytecode 存储真正的字节码指令。 +- exception table 表示方法内部的异常信息。 +- maximum stack size 表示操作数栈的最大深度,方法执行的任意期间操作数栈深度都不会超过这个值。 +- maximum local variable 表示临时变量表的大小,注意,并不等于方法中所有临时变量的数量之和,当一个作用域结束,内部的临时变量占用的位置就会被替换掉。 +- code length 表示字节码指令的长度。 + +对应 class 文件中的位置如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-file-jiegou-b5853549-b17b-48eb-8eb3-a393fb5d655f.png) + +到此为止,class 文件的内部算是剖析得差不多了,希望能对大家有所帮助。第一次拿刀,手有点颤,如果哪里有不足的地方,欢迎大家在评论区毫不留情地指出来! + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/class-load.md b/docs/jvm/class-load.md new file mode 100644 index 0000000000..d14be0c6a2 --- /dev/null +++ b/docs/jvm/class-load.md @@ -0,0 +1,187 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# 我竟然不再抗拒Java的类加载机制了 + + +### 01、字节码 + +在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,因为它和类加载机制息息相关。 + +计算机只认识 0 和 1,所以任何语言编写的程序都需要编译成机器码才能被计算机理解,然后执行,Java 也不例外。 + +Java 在诞生的时候喊出了一个非常牛逼的口号:“Write Once, Run Anywhere”,为了达成这个目的,Sun 公司发布了许多可以在不同平台(Windows、Linux)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-load-01.png) + + +到底 Java 字节码是什么样子,我们借助一段简单的代码来看一看。 + +源码如下: + +```java +package com.cmower.java_demo; + +public class Test { + + public static void main(String[] args) { + System.out.println("沉默王二"); + } + +} +``` + +代码编译通过后,通过 `xxd Test.class` 命令查看一下这个字节码文件。 + +``` +xxd Test.class +00000000: cafe babe 0000 0034 0022 0700 0201 0019 .......4."...... +00000010: 636f 6d2f 636d 6f77 6572 2f6a 6176 615f com/cmower/java_ +00000020: 6465 6d6f 2f54 6573 7407 0004 0100 106a demo/Test......j +00000030: 6176 612f 6c61 6e67 2f4f 626a 6563 7401 ava/lang/Object. +00000040: 0006 3c69 6e69 743e 0100 0328 2956 0100 .....()V.. +00000050: 0443 6f64 650a 0003 0009 0c00 0500 0601 .Code........... +00000060: 000f 4c69 6e65 4e75 6d62 6572 5461 626c ..LineNumberTabl +``` + +感觉有点懵逼,对不对? + +懵就对了。 + +这段字节码中的 `cafe babe` 被称为“魔数”,是 JVM 识别 .class 文件的标志。文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 `8950 4e47 `。 + +至于其他内容嘛,可以选择忘记了。 + +### 02、类加载过程 + +了解了 Java 字节码后,我们来聊聊 Java 的类加载过程。 + +Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 + +1)Loading(载入) + +JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 `java.lang.Class` 对象。 + +2)Verification(验证) + +JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。 + +- 确保二进制字节流格式符合预期(比如说是否以 `cafe bene` 开头)。 +- 是否所有方法都遵守访问控制关键字的限定。 +- 方法调用的参数个数和类型是否正确。 +- 确保变量在使用之前被正确初始化了。 +- 检查变量是否被赋予恰当类型的值。 + +3)Preparation(准备) + +JVM 会在该阶段对类变量(也称为静态变量,`static` 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。 + +也就是说,假如有这样一段代码: + +```java +public String chenmo = "沉默"; +public static String wanger = "王二"; +public static final String cmower = "沉默王二"; +``` + +chenmo 不会被分配内存,而 wanger 会;但 wanger 的初始值不是“王二”而是 `null`。 + +需要注意的是,`static final` 修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 cmower 在准备阶段的值为“沉默王二”而不是 `null`。 + +4)Resolution(解析) + +该阶段将常量池中的符号引用转化为直接引用。 + +what?符号引用,直接引用? + +**符号引用**以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。 + +在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 `com.Wanger` 类引用了 `com.Chenmo` 类,编译时 Wanger 类并不知道 Chenmo 类的实际内存地址,因此只能使用符号 `com.Chenmo`。 + +**直接引用**通过对符号引用进行解析,找到引用的实际内存地址。 + +5)Initialization(初始化) + +该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。 + +oh,no,上面这段话说得很抽象,不好理解,对不对,我来举个例子。 + +```java +String cmower = new String("沉默王二"); +``` + +上面这段代码使用了 `new` 关键字来实例化一个字符串对象,那么这时候,就会调用 String 类的构造方法对 cmower 进行实例化。 + +### 03、类加载器 + +聊完类加载过程,就不得不聊聊类加载器。 + +一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试 + `ClassNotFoundException` 和 `NoClassDefFoundError` 等异常。 + +对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 `equals`)。 + +站在程序员的角度来看,Java 类加载器可以分为三种。 + +1)启动类加载器(Bootstrap Class-Loader),加载 `jre/lib` 包下面的 jar 文件,比如说常见的 rt.jar。 + +2)扩展类加载器(Extension or Ext Class-Loader),加载 `jre/lib/ext` 包下面的 jar 文件。 + +3)应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。 + +来来来,通过一段简单的代码了解下。 + +```java +public class Test { + + public static void main(String[] args) { + ClassLoader loader = Test.class.getClassLoader(); + while (loader != null) { + System.out.println(loader.toString()); + loader = loader.getParent(); + } + } + +} +``` + +每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 `类名.class.getClassLoader()` 可以获取到此引用;然后通过 `loader.getParent()` 可以获取类加载器的上层类加载器。 + +这段代码的输出结果如下: + +``` +sun.misc.Launcher$AppClassLoader@73d16e93 +sun.misc.Launcher$ExtClassLoader@15db9742 +``` + +第一行输出为 Test 的类加载器,即应用类加载器,它是 `sun.misc.Launcher$AppClassLoader` 类的实例;第二行输出为扩展类加载器,是 `sun.misc.Launcher$ExtClassLoader` 类的实例。那启动类加载器呢? + +按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 `getParent()` 返回 `null`。所以没有输出。 + + +### 04、双亲委派模型 + +如果以上三种类加载器不能满足要求的话,程序员还可以自定义类加载器(继承 `java.lang.ClassLoader` 类),它们之间的层级关系如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/class-load-02.png) + +这种层次关系被称作为**双亲委派模型**:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。 + +PS:双亲委派模型突然让我联想到朱元璋同志,这个同志当上了皇帝之后连宰相都不要了,所有的事情都亲力亲为,只有自己没精力没时间做的事才交给大臣们去干。 + +使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。 + +上文中曾提到,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。 + +### 05、最后 + +硬着头皮翻看了大量的资料,并且动手去研究以后,我发现自己竟然对 Java 类加载机制(JVM 将类的信息动态添加到内存并使用的一种机制)不那么抗拒了——真是蛮奇妙的一件事啊。 + +也许学习就应该是这样,只要你敢于挑战自己,就能收获知识——就像山就在那里,只要你肯攀登,就能到达山顶。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/compile-jdk.md b/docs/jvm/compile-jdk.md new file mode 100644 index 0000000000..3a0744b12b --- /dev/null +++ b/docs/jvm/compile-jdk.md @@ -0,0 +1,362 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# 自己编译JDK + + +引用链接:https://segmentfault.com/a/1190000023251649 + + +很多小伙伴们做`Java`开发,天天写`Java`代码,肯定离不开`Java`基础环境:`JDK`,毕竟我们写好的`Java`代码也是跑在`JVM`虚拟机上。 + +一般来说,我们学`Java`之前,第一步就是安装`JDK`环境。这个简单啊,我们一般直接把`JDK`从官网下载下来,安装完成,配个环境变量就可以愉快地使用了。 + +不过话说回来,对于这个天天使用的东西,我们难道不好奇这玩意儿它到底是怎么由源码编译出来的吗? + +带着这个原始的疑问,今天准备大干一场,自己动动呆萌的小手,来编译一个属于自己的`JDK`吧! + +## 环境准备 + +> 首选说在前面的是,编译前的软件版本关系极其重要,自己在踩坑时,所出现的各种奇奇怪怪的问题几乎都和这个有关,后来版本匹配之后,就非常顺利了。 + +我们来**盘点和梳理**一下编译一个JDK需要哪些环境和工具: + +### **1、boot JDK** + +我们要想编译`JDK`,首先自己本机必须提前已经安装有一个`JDK`,官方称之为`bootstrap JDK`(或者称为`boot JDK`)。 + +比如想编译`JDK 8`,那本机必须最起码得有一个`JDK 7`或者更新一点的版本;你想编译`JDK 11`,那就要求本机必须装有`JDK 10`或者`11`。 + +> 所以鸡生蛋、蛋生鸡又来了... + +### **2、Unix环境** + +编译`JDK`需要`Unix`环境的支持! + +这一点在`Linux`操作系统和`macOS`操作系统上已经天然的保证了,而对于`Windows`兄弟来说稍微麻烦一点,需要通过使用`Cygwin`或者`MinGW/MSYS`这种软件来模拟。 + +就像官方所说:在`Linux`平台编译`JDK`一般问题最少,容易成功;`macOS`次之;`Windows`上则需要稍微多花点精力,问题可能也多一些。 + +究其本质原因,还是因为`Windows`毕竟不是一个`Unix-Like`内核的系统,毕竟很多软件的原始编译都离不开`Unix Toolkit`,所以相对肯定要麻烦一些。 + +### **3、编译器/编译工具链** + +`JDK`底层源码(尤其`JVM`虚拟机部分)很多都是`C++/C`写的,所以相关编译器也跑不掉。 + +一图胜千言,各平台上的编译器支持如下表所示,按平台选择即可: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-3b66d5b6-272f-47bd-88f7-47146a06ef06.png) + + +### **4、其他工具** + +典型的比如: + +* `Autoconf`:软件源码包的自动配置工具 +* `Make`:编译构建工具 +* `freetype`:一个免费的渲染库,`JDK`图形化部分的代码可能会用它 + +好,环境盘点就到这里,接下来具体列一下我在编译`JDK 8`和`JDK 11`时分别用到的软件详细版本信息: + +**编译JDK 8时:** + +* `操作系统`:macOS 10.11.6 +* `boot JDK`:JDK 1.8.0 (build 1.8.0_201-b09) +* `Xcode版本`:8.2 +* `编译器`:Version 8.0.0 (at /usr/bin/clang) + +**编译JDK 11时:** + +* `操作系统`:macOS 10.15.4 +* `boot JDK`:JDK 11.0.7 (build 11.0.7+8-LTS) +* `Xcode版本`:11.5 +* `编译器`:Version 11.0.3 (at /usr/bin/clang) + +大家在编译时如果过程中有很多问题,大概率少软件没装,或者软件版本不匹配,不要轻易放弃,需要耐心自查一下。 + +* * * + +## 下载JDK源码 + +下载`JDK`源码其实有两种方式。 + +### **方式一:通过Mercurial工具下载** + +`Mercurial`可以理解为和`Git`一样,是另外一种代码管理工具,安装好之后就有一个`hg`命令可用。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-cd8a19ba-e9f5-4a4a-a23c-17688f0f459d.png) + + +而`OpenJDK`的源码已经提前托管到`http://hg.openjdk.java.net/`。 + +因此,比如下载`JDK 8`,可直接`hg clone`一下就行,和`git clone`一样: + +`hg clone [http://hg.openjdk.java.net/jd...](https://link.segmentfault.com/?enc=Snt8gNbYV7nkV3etTe%2FGJw%3D%3D.7IrUNCuc0HOEyvjCiCBOPMEBJ09bjLifieJi0I7iwtuuIeYUdSfCkC9c4D7z9wdq) +` + +同理,下载`JDK 11`: + +`hg clone [http://hg.openjdk.java.net/jd...](https://link.segmentfault.com/?enc=BnHqAYXzfRcVfPgGgo1yOw%3D%3D.011np6%2FiCLuojl%2FBtvROkTVXr0PSdMYcYpAg2WUIE045BEFIrbCNAD42vWwIUb3d) +` + +但是这种方式下载速度不是很快。 + +### **方式二:直接下载打包好的源码包** + +下载地址:`https://jdk.java.net/` + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-1bbbb1f8-da01-46e1-a793-487a25193c68.png) + + +选择你想要的版本下载即可。 + +* * * + +### 编译前的自动配置 + +源码包下载好,放到本地某个目录(建议路径纯英文,避免不必要的麻烦),解压之,然后进入源码根目录,执行: + +`sh configure +` + +> 当然这里运行的是默认配置项。 + +这一步会进行一系列的自动配置工作,时间一般很快,最终如果能出现一下提示,那么很幸运,编译前的配置工作就完成了! + +这里我给出我自己分别在配置`JDK 11`和`JDK 8`时候完成时的样子: + +**配置JDK 8完成:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-27593edc-03e2-4a42-baf3-ed5e5096b3cb.png) + + +**配置JDK 11完成:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-8526d944-a36e-4d37-93a0-9ad4ad53f927.png) + + +**注:** 如果这一步出错,大概率是某个软件环境未装,或者即使装了,但版本不匹配,控制台打印日志里一般是会提醒的。 + +比如我在配置`JDK 8`的时候,就遇到了一个`errof:GCC compiler is required`的问题: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-2957399f-6451-46dc-a003-76e5159265e9.png) + + +明明系统里已经有编译器,但还是报这个错误。通过后来修改 `jdk源码根目录/common/autoconf/generated-configure.sh`文件,将相关的两行代码注释后就配置通过了 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-ffa10d36-3a77-48aa-ae0c-d3daf67f9a19.png) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-a6f6e416-639e-4706-8b40-6152eb3cf85d.png) + + +配置完成,接下来开始执行真正的编译动作了! + +* * * + +## 真正的编译动作 + +我们这里进行的是全量编译,直接在我们下载的`JDK`源码根目录下执行如下命令即可: + +`make all +` + +这一步编译需要一点时间,耐心等待一下即可。编译过程如果有错误,会终止编译,如果能看到如下两个画面,那么则恭喜你,自己编译`JDK`源码就已经通过了,可以搞一杯咖啡庆祝一下了。 + +**JDK 8编译完成:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-89020f5a-0909-4c57-8c88-f655293a42a4.png) + + +**JDK 11编译完成:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-993fac94-2473-4f3b-9737-959510d2fe98.png) + + +从两张图的对比可以看出,编译`JDK 8`和`JDK 11`完成时在输出上还是有区别的。时间上的区别很大程度上来源于`JDK 11`的编译机配置要高不少。 + +* * * + +## 验证成果 + +`JDK`源码编译完成之后肯定会产生和输出很多产物,这也是我们所迫不及待想看到的。 + +由于`JDK 8`和`JDK 11`的源码包组织结构并不一样,所以输出东西的内容和位置也有区别。我们一一来盘点一下。 + +### **1、JDK 8的编译输出** + +编译完成,`build`目录下会生成一个`macosx-x86_64-normal-server-release`目录,所有的编译成果均位于其中。 + +首先,编译出来的`Java`可执行程序可以在如下目录里找到: + +`jdk源码根目录/build/macosx-x86_64-normal-server-release/jdk/bin` + +进入该目录后,可以输入`./java -version`命令验证: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-f02dff40-f27e-476c-998b-bd6cdb5d3559.png) + + +其次,编译生成的成品`JDK`套装,可以在目录 + +`jdk源码根目录/build/macosx-x86_64-normal-server-release/images +` + +下找到,如图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-1c781d34-776e-4acc-8d2b-b34bc59fda61.png) + + +其中: + +* `j2sdk-image`:编译生成的JDK +* `j2re-image`:编译生成的JRE + +进入`j2sdk-image`目录会发现,里面的内容和我们平时从网络上下载的成品`JDK`内容一致。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-7b7f147e-58c9-4eb5-b407-b8984cd72e1d.png) + + +### **2、JDK 11的编译输出** + +> JDK 11的源码目录组织方式和JDK 8本身就有区别,编译生成的产物和上面编译JDK 8的输出有一定区别,但也不大。 + +`JDK 11`编译完成,同样在`build`目录下会生成一个`macosx-x86_64-normal-server-release`目录,所有的编译成果均位于其中。 + +同样编译出来的Java可执行程序可以在目录 + +`JDK源码根目录/build/macosx-x86_64-normal-server-release/jdk/bin` + +下看到,进入该目录后,也可以输入`./java -version`命令验证: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-f9b55425-f308-44e8-8812-ac59b2707c81.png) + + +其次,编译生成的成品`JDK 11`套装,可以在目录 + +`JDK源码根目录/build/macosx-x86_64-normal-server-release/images +` + +下找到,如图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-4e96858f-f681-4498-b1c4-282d317a6a32.png) + + +其中`jdk`目录就是编译生成的成品`JDK 11`套装。 + +* * * + +## 使用自己编译的JDK + +既然我们已经动手编译出了`JDK`成品,接下来我们得用上哇。 + +新建一个最最基本的`Java`工程,比如命名为`JdkTest`,目的是把我们自己编译出的`JDK`给用上。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-2cf54b29-9b7e-46b2-8cde-4c36960aa09b.png) + + +我们点开`Project Structure`,选到`SDKs`选项,新添加上自己刚刚编译生成的JDK,并选为项目的JDK,看看是否能正常工作 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-ad8023d0-fbb7-48b1-856e-a8818677a0a5.png) + + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-b8d87f09-6178-44c7-9572-a2852e81318d.png) + + +点击确定之后,我们运行之: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-3164cf31-8078-46d7-bee0-22e05b0c08de.png) + + +可以看到我们自己编译出的JDK已经用上了。 + +* * * + +## 关联JDK源码并修改 + +我们继续在上一步`JdkTest`项目的`Project Structure` → `SDKs`里将`JDK`源码关联到自行下载的JDK源码路径上: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-129ede68-c368-461e-92d6-38a8e5dee344.png) + + +这样方便我们对自己下载的`JDK源码`进行**阅读**、**调试**、**修改**、以及在源码里随意**做笔记**和**加注释**。 + +举个最简单的例子,比如我们打开`System.out.println()`这个函数的底层源码: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-c406b2a2-208a-4a54-a869-b3f526e93ccd.png) + + +我们随便给它修改一下,加两行简单的标记,像这样: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-2a46b215-04e5-458a-b475-4dc31d7fe326.png) + + +为了使我们新加的代码行生效,我们必须要重新去JDK源码的根目录中再次执行 `make images`重新编译生成JDK方可生效: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-fd3cf88d-007e-4615-99b5-a3499c35ef40.png) + + +因为之前已经全量编译过了,所以再次`make`的时候增量编译一般很快。 + +重新编译之后,我们再次运行`JdkTest`项目,就可以看到改动的效果了: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-33dc0de6-2690-4ba2-9fe7-0e450c44c07b.png) + + +* * * + +## 多行注释的问题 + +记得之前搭建[《JDK源码阅读环境》](https://link.segmentfault.com/?enc=JrtwTM%2BhTi%2B7DiRtBYeZSQ%3D%3D.T2U2BwPhK3iqeNk%2B%2BMuGttrqlD2zy9v1C%2BqYPvIYEvcvkTe1xyPrnnb%2FdaTGkBqY)时,大家可能发现了一个问题:阅读源码嘛,给源代码做点注释或笔记很常见!但那时候有个问题就是做注释时**不可改变代码的行结构**(只能行尾注释,不能跨行注释),否则debug调试时会出现**行号错位**的问题。 + +原因很简单,因为我们虽然做了源代码目录的映射,但是实际支撑运行的`JDK`还是预先安装好的那个JDK环境,并不是根据我们修改后的源码来重新编译构建的,所以看到这里,解决这个问题就很简单,就像上面一样自行编译一下`JDK`即可。 + +实际在实验时,还有一个很典型的问题是,当添加了多行的中文注释后,再编译居然会报错! + +比如,还是以上面例子中最简单的`System.out.println()`源码为例,我们添加几行中文注释: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-d6a44833-b908-4824-8862-1679bfdddfa3.png) + + +这时候我们去JDK源码目录下编译会发现满屏类似这样的报错: + +> 错误: 编码 ascii 的不可映射字符 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-ad0ca5a3-36c7-477d-bd58-d6731a87d762.png) + + +顿时有点懵,毕竟仅仅是加了几行注释。对于我们来说,源码里写点多行的中文注释基本是**刚需**,然而编译竟会报错,这还能不能让人愉快的玩耍了... 当时后背有点发凉。 + +实不相瞒,就这个问题排查了一段时间,熬到了很晚。最终折腾了一番,通过如下这种方式解决了,顺便分享给小伙伴们,大家如果遇到了这个问题,可以参考着解决一下。 + +因为从控制台的报错可以很明显的看出,肯定是字符编码相关的问题导致的,而且都指向了`ascii`这种编码方式。 + +于是将JDK的源码从根目录导入了Vs Code,然后全目录查找`encoding ascii`相关的内容,看看有没有什么端倪,结果发现 + +`jdk源码根目录/make/common/SetupJavaCompilers.gmk`文件中有两处指定了`ascii`相关的编码方式: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-a86933af-f6d5-4d45-b4ca-33069a212c52.png) + + +于是尝试将这两处`-encoding ascii`的均替换成`-encoding utf-8`: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/compile-jdk-c8117faf-d027-48d3-869b-32d0e98e8372.png) + + +然后再次执行`make images`编译,编译顺利通过! + + +至此大功告成! + +这样后面不管是**阅读**、**调试**还是**定制**`JDK`源码都非常方便了。 + +--- + +引用链接:https://segmentfault.com/a/1190000023251649 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/jvm/cpu-percent-100.md b/docs/jvm/cpu-percent-100.md new file mode 100644 index 0000000000..f84b79493e --- /dev/null +++ b/docs/jvm/cpu-percent-100.md @@ -0,0 +1,156 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# 一次生产CPU 100% 排查优化实践 + +## 前言 + +最近又收到了运维报警:表示有些服务器负载非常高,让我们定位问题。 + + +## 定位问题 + +拿到问题后首先去服务器上看了看,发现运行的只有我们的 Java 应用。于是先用 `ps` 命令拿到了应用的 `PID`。 + +接着使用 `top -Hp pid` 将这个进程的线程显示出来。输入大写的 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-e9b35104-fce9-40ea-ae91-8bbb7fd8aa96.jpg) + +果然某些线程的 CPU 使用率非常高。 + + +为了方便定位问题我立马使用 `jstack pid > pid.log` 将线程栈 `dump` 到日志文件中。 + +我在上面 100% 的线程中随机选了一个 `pid=194283` 转换为 16 进制(2f6eb)后在线程快照中查询: + +> 因为线程快照中线程 ID 都是16进制存放。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-f8b051d5-f28d-481e-a0b2-e97151797e3b.jpg) + +发现这是 `Disruptor` 的一个堆栈,前段时间正好解决过一个由于 Disruptor 队列引起的一次 [OOM]():[强如 Disruptor 也发生内存溢出?](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/) + +没想到又来一出。 + +为了更加直观的查看线程的状态信息,我将快照信息上传到专门分析的平台上。 + +[http://fastthread.io/](http://fastthread.io/) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-d6c9bc1c-9600-47f2-9ff1-d0c9bd8ef849.jpg) + +其中有一项菜单展示了所有消耗 CPU 的线程,我仔细看了下发现几乎都是和上面的堆栈一样。 + +也就是说都是 `Disruptor` 队列的堆栈,同时都在执行 `java.lang.Thread.yield` 函数。 + +众所周知 `yield` 函数会让当前线程让出 `CPU` 资源,再让其他线程来竞争。 + +根据刚才的线程快照发现处于 `RUNNABLE` 状态并且都在执行 `yield` 函数的线程大概有 30几个。 + +因此初步判断为大量线程执行 `yield` 函数之后互相竞争导致 CPU 使用率增高,而通过对堆栈发现是和使用 `Disruptor` 有关。 + +## 解决问题 + +而后我查看了代码,发现是根据每一个业务场景在内部都会使用 2 个 `Disruptor` 队列来解耦。 + +假设现在有 7 个业务类型,那就等于是创建 `2*7=14` 个 `Disruptor` 队列,同时每个队列有一个消费者,也就是总共有 14 个消费者(生产环境更多)。 + +同时发现配置的消费等待策略为 `YieldingWaitStrategy` 这种等待策略确实会执行 yield 来让出 CPU。 + +代码如下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-49840c0d-2c10-4bcb-80c6-1df7553ddb6c.jpg) + +> 初步看来和这个等待策略有很大的关系。 + +### 本地模拟 + +为了验证,我在本地创建了 15 个 `Disruptor` 队列同时结合监控观察 CPU 的使用情况。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-7f3b2fa6-6505-4b67-9f42-0170a236832b.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-d597089d-54e0-49ef-a0f9-41798e84de48.jpg) + +创建了 15 个 `Disruptor` 队列,同时每个队列都用线程池来往 `Disruptor队列` 里面发送 100W 条数据。 + +消费程序仅仅只是打印一下。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-97b88b4d-2d81-47ab-9beb-830ac122c282.jpg) + +跑了一段时间发现 CPU 使用率确实很高。 + +--- + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-c0ee1da2-29af-4581-b0d8-97f6250401e7.jpg) + +同时 `dump` 线程发现和生产的现象也是一致的:消费线程都处于 `RUNNABLE` 状态,同时都在执行 `yield`。 + +通过查询 `Disruptor` 官方文档发现: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-de904a90-8b59-4333-82f5-9ec94a6525a0.jpg) + +> YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用`自旋 + yield`的方式来提高性能。 +> 当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。 + +--- + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-3faf6f7e-0d2c-4cfe-8e3a-07e15601485d.jpg) + +同时查阅到其他的等待策略 `BlockingWaitStrategy` (也是默认的策略),它使用的是锁的机制,对 CPU 的使用率不高。 + +于是在和之前同样的条件下将等待策略换为 `BlockingWaitStrategy`。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-12912ce3-a702-4bb2-a19b-816c22f7d43a.jpg) + +--- + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-b4aad83e-af9d-48fc-bcd0-ad2a42588179.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-56dc1513-8f10-422f-bb2a-ae5dcfb8413f.jpg) + +和刚才的 CPU 对比会发现到后面使用率的会有明显的降低;同时 dump 线程后会发现大部分线程都处于 waiting 状态。 + + +### 优化解决 + +看样子将等待策略换为 `BlockingWaitStrategy` 可以减缓 CPU 的使用, + +但留意到官方对 `YieldingWaitStrategy` 的描述里谈道: +当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。 + +而现有的使用场景很明显消费线程数已经大大的超过了核心 CPU 数了,因为我的使用方式是一个 `Disruptor` 队列一个消费者,所以我将队列调整为只有 1 个再试试(策略依然是 `YieldingWaitStrategy`)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-b1cbc2c2-828a-46e8-ba14-86cd0fa660c6.jpg) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/cpu-percent-100-f8fb7682-a61a-407d-923c-890a16bce109.jpg) + +跑了一分钟,发现 CPU 的使用率一直都比较平稳而且不高。 + +## 总结 + +所以排查到此可以有一个结论了,想要根本解决这个问题需要将我们现有的业务拆分;现在是一个应用里同时处理了 N 个业务,每个业务都会使用好几个 `Disruptor` 队列。 + +由于是在一台服务器上运行,所以 CPU 资源都是共享的,这就会导致 CPU 的使用率居高不下。 + +所以我们的调整方式如下: + +- 为了快速缓解这个问题,先将等待策略换为 `BlockingWaitStrategy`,可以有效降低 CPU 的使用率(业务上也还能接受)。 +- 第二步就需要将应用拆分(上文模拟的一个 `Disruptor` 队列),一个应用处理一种业务类型;然后分别单独部署,这样也可以互相隔离互不影响。 + +当然还有其他的一些优化,因为这也是一个老系统了,这次 dump 线程居然发现创建了 800+ 的线程。 + +创建线程池的方式也是核心线程数、最大线程数是一样的,导致一些空闲的线程也得不到回收;这样会有很多无意义的资源消耗。 + +所以也会结合业务将创建线程池的方式调整一下,将线程数降下来,尽量的物尽其用。 + + +本文的演示代码已上传至 GitHub: + +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor) + +**你的点赞与分享是对我最大的支持** + +原文链接:[https://github.com/crossoverJie/JCSprout/blob/master/docs/jvm/cpu-percent-100.md](https://github.com/crossoverJie/JCSprout/blob/master/docs/jvm/cpu-percent-100.md) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/gc.md b/docs/jvm/gc.md new file mode 100644 index 0000000000..1509587395 --- /dev/null +++ b/docs/jvm/gc.md @@ -0,0 +1,234 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# 咱们从头到尾说一次Java垃圾回收 + + +之前上学的时候有这个一个梗,说在食堂里吃饭,吃完把餐盘端走清理的,是 C++ 程序员,吃完直接就走的,是 Java 程序员。 + +确实,在 Java 的世界里,似乎我们不用对垃圾回收那么的专注,很多初学者不懂 GC,也依然能写出一个能用甚至还不错的程序或系统。但其实这并不代表 Java 的 GC 就不重要。相反,它是那么的重要和复杂,以至于出了问题,那些初学者除了打开 GC 日志,看着一堆0101的天文,啥也做不了。 + +今天我们就从头到尾完整地聊一聊 Java 的垃圾回收。 + +### 什么是垃圾回收 + +* 垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。 +* Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾,C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的 allocated,然后不停的析构。于是,有人就提出,能不能写一段程序实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢? +* 1960年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时 Java 还没有出世呢!所以实际上 GC 并不是Java的专利,GC 的历史远远大于 Java 的历史! + +### 怎么定义垃圾 + +既然我们要做垃圾回收,首先我们得搞清楚垃圾的定义是什么,哪些内存是需要回收的。 + +**引用计数算法** +引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,那么该对象就会被回收。 + +**String m = new String("jack");** + +先创建一个字符串,这时候"jack"有一个引用,就是 m。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-691109d2-bee4-4a79-8da6-87c5fd233f54.jpg) + +然后将 m 设置为 null,这时候"jack"的引用次数就等于0了,在引用计数算法中,意味着这块内容就需要被回收了。 + +**m = null;** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-74865618-4576-4f8b-baf3-17d6a71125b9.jpg) + +引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。 + +看似很美好,但我们知道JVM的垃圾回收就是"Stop-The-World"的,那是什么原因导致我们最终放弃了引用计数算法呢?看下面的例子。 + +```java +public class ReferenceCountingGC { + +public Object instance; + +public ReferenceCountingGC(String name){} +} + +public static void testGC(){ + +ReferenceCountingGC a = new ReferenceCountingGC("objA"); +ReferenceCountingGC b = new ReferenceCountingGC("objB"); + +a.instance = b; +b.instance = a; + +a = null; +b = null; +} +``` + +**1\. 定义2个对象** +**2\. 相互引用** +**3\. 置空各自的声明引用** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-fe980c00-3605-4b5d-a711-7edbfd2c80b0.jpg) + +我们可以看到,最后这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。 + +**可达性分析算法** + +可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-1636ce77-77b3-4b10-b75a-c0c2d28912c5.jpg) + +通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于 GC Root。 + +**Java 内存区域** + +在 Java 语言中,可作为 GC Root 的对象包括以下4种: + +* 虚拟机栈(栈帧中的本地变量表)中引用的对象 +* 方法区中类静态属性引用的对象 +* 方法区中常量引用的对象 +* 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-6abf9f50-dc53-4e8f-a7f6-3e74df8803d6.jpg) + +1、虚拟机栈(栈帧中的本地变量表)中引用的对象 +此时的 s,即为 GC Root,当s置空时,localParameter 对象也断掉了与 GC Root 的引用链,将被回收。 + +```java +public class StackLocalParameter { +public StackLocalParameter(String name){} +} + +public static void testGC(){ +StackLocalParameter s = new StackLocalParameter("localParameter"); +s = null; +} +``` + +2、方法区中类静态属性引用的对象 +s 为 GC Root,s 置为 null,经过 GC 后,s 所指向的 properties 对象由于无法与 GC Root 建立关系被回收。 + +而 m 作为类的静态属性,也属于 GC Root,parameter 对象依然与 GC root 建立着连接,所以此时 parameter 对象并不会被回收。 + +```java +public class MethodAreaStaicProperties { +public static MethodAreaStaicProperties m; +public MethodAreaStaicProperties(String name){} +} + +public static void testGC(){ +MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties"); +s.m = new MethodAreaStaicProperties("parameter"); +s = null; +} +``` + +3、方法区中常量引用的对象 +m 即为方法区中的常量引用,也为 GC Root,s 置为 null 后,final 对象也不会因没有与 GC Root 建立联系而被回收。 + +```java +public class MethodAreaStaicProperties { +public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final"); +public MethodAreaStaicProperties(String name){} +} + +public static void testGC(){ +MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties"); +s = null; +} +``` + +4、本地方法栈中引用的对象 +任何 native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-a138a4b4-56cb-4d6f-a65a-7f4259977476.jpg) + +### 怎么回收垃圾 + +在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,这里我们讨论几种常见的垃圾收集算法的核心思想。 + +**标记 --- 清除算法** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-2001e224-0f34-4429-bc89-a8fbe8ab271c.jpg) + +标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。就像上图一样,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。 + +这逻辑再清晰不过了,并且也很好操作,但它存在一个很大的问题,那就是内存碎片。 + +上图中等方块的假设是 2M,小一些的是 1M,大一些的是 4M。等我们回收完,内存就会切成了很多段。我们知道开辟内存空间时,需要的是连续的内存区域,这时候我们需要一个 2M的内存区域,其中有2个 1M 是没法用的。这样就导致,其实我们本身还有这么多的内存的,但却用不了。 + +**复制算法** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-a2b15e6f-6921-4710-bf76-77858df38c27.jpg) + +复制算法(Copying)是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效。 + +上面的图很清楚,也很明显的暴露了另一个问题,合着我这140平的大三房,只能当70平米的小两房来使?代价实在太高。 + +**标记整理算法** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-2d47a225-ad9d-4f15-9b4d-7dce9a693adf.jpg) + +标记整理算法(Mark-Compact)标记过程仍然与标记 --- 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。 + +标记整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。 + +分代收集算法分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记 --- 整理算法来进行回收。so,另一个问题来了,那内存区域到底被分为哪几块,每一块又有什么特别适合什么算法呢? + +### 内存模型与回收策略 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/gc-59dddea1-b6bc-4fd4-bb79-d81adbdc7bed.jpg) + +Java 堆(Java Heap)是JVM所管理的内存中最大的一块,堆又是垃圾收集器管理的主要区域,这里我们主要分析一下 Java 堆的结构。 + +Java 堆主要分为2个区域-年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。可能这时候大家会有疑问,为什么需要 Survivor 区,为什么Survivor 还要分2个区。不着急,我们从头到尾,看看对象到底是怎么来的,而它又是怎么没的。 + +**Eden 区** + +IBM 公司的专业研究表明,有将近98%的对象是朝生夕死,所以针对这一现状,大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。 + +通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。 + +**Survivor 区** + +Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。 + +1、为啥需要? + +不就是新生代到老年代么,直接 Eden 到 Old 不好了吗,为啥要这么复杂。想想如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。 + +所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。 + +2、为啥需要俩? + +设置两个 Survivor 区最大的好处就是解决内存碎片化。 + +我们先假设一下,Survivor 如果只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有2个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。 + +这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的最佳方案。 + +**Old 区** + +老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记 --- 整理算法。 + +除了上述所说,在内存担保机制下,无法安置的对象会直接进到老年代,以下几种情况也会进入老年代。 + +1、大对象 + +大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进到老年代。这样做主要是为了避免在 Eden 区及2个 Survivor 区之间发生大量的内存复制。当你的系统有非常多“朝生夕死”的大对象时,得注意了。 + +2、长期存活对象 + +虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中没经历一次 Minor GC,年龄就增加1岁。当年龄增加到15岁时,这时候就会被转移到老年代。当然,这里的15,JVM 也支持进行特殊设置。 + +3、动态对象年龄 + +虚拟机并不重视要求对象年龄必须到15岁,才会放入老年区,如果 Survivor 空间中相同年龄所有对象大小的综合大于 Survivor 空间的一般,年龄大于等于该年龄的对象就可以直接进去老年区,无需等你“成年”。 + +这其实有点类似于负载均衡,轮询是负载均衡的一种,保证每台机器都分得同样的请求。看似很均衡,但每台机的硬件不通,健康状况不同,我们还可以基于每台机接受的请求数,或每台机的响应时间等,来调整我们的负载均衡算法。 + +>- 整理:沉默王二,原文链接:https://zhuanlan.zhihu.com/p/73628158 +>- https://segmentfault.com/a/1190000038256027 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/how-jvm-run-zijiema-zhiling.md b/docs/jvm/how-jvm-run-zijiema-zhiling.md new file mode 100644 index 0000000000..8ffd97a1fb --- /dev/null +++ b/docs/jvm/how-jvm-run-zijiema-zhiling.md @@ -0,0 +1,587 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# 虚拟机是如何执行字节码指令的? + + +执行引擎是 Java 虚拟机最核心的组成部分之一。「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力,区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机执行引擎是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。 + +在 Java 虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现里,执行引擎在执行 Java 代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种方式,也可能两者都有,甚至还可能会包含几个不同级别的编译器执行引擎。但从外观上来看,所有 Java 虚拟机的执行引擎是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。 + +### 一. 运行时栈帧结构 + +栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量、操作数栈、动态链接和方法返回地址等信息。**每一个方法从调用开始到执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。** + +每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。 + +一个线程中的方法调用链可能会很长,很多方法都处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法成为当前方法。执行引擎运行的所有字节码指令对当前栈帧进行操作,在概念模型上,典型的栈帧结构如下图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-a58ee82e-c0b0-4c06-9606-f7a0f0df0de9) + +#### 局部变量表 + +局部变量表(Local Variables Table)用来保存方法中的局部变量,以及方法参数。当 Java 源代码文件被编译成 class 文件的时候,局部变量表的最大容量就已经确定了。 + +我们来看这样一段代码。 + +```java +public class LocalVaraiablesTable { + private void write(int age) { + String name = "沉默王二"; + } +} +``` + +`write()` 方法有一个参数 age,一个局部变量 name。 + +然后用 Intellij IDEA 的 jclasslib 查看一下编译后的字节码文件 LocalVaraiablesTable.class。可以看到 `write()` 方法的 Code 属性中,Maximum local variables(局部变量表的最大容量)的值为 3。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-70ab6bf6-4fbb-4722-99b4-a93d5061630c.png) + +按理说,局部变量表的最大容量应该为 2 才对,一个 age,一个 name,为什么是 3 呢? + +当一个成员方法(非静态方法)被调用时,第 0 个变量其实是调用这个成员方法的对象引用,也就是那个大名鼎鼎的 this。调用方法 `write(18)`,实际上是调用 `write(this, 18)`。 + +点开 Code 属性,查看 LocalVaraiableTable 就可以看到详细的信息了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-e5e6037c-9be1-472f-8ab3-2754466e7828.png) + +第 0 个是 this,类型为 LocalVaraiablesTable 对象;第 1 个是方法参数 age,类型为整型 int;第 2 个是方法内部的局部变量 name,类型为字符串 String。 + +当然了,局部变量表的大小并不是方法中所有局部变量的数量之和,它与变量的类型和变量的作用域有关。当一个局部变量的作用域结束了,它占用的局部变量表中的位置就被接下来的局部变量取代了。 + +来看下面这段代码。 + +```java +public static void method() { + // ① + if (true) { + // ② + String name = "沉默王二"; + } + // ③ + if(true) { + // ④ + int age = 18; + } + // ⑤ +} +``` + +- `method()` 方法的局部变量表大小为 1,因为是静态方法,所以不需要添加 this 作为局部变量表的第一个元素; +- ②的时候局部变量有一个 name,局部变量表的大小变为 1; +- ③的时候 name 变量的作用域结束; +- ④的时候局部变量有一个 age,局部变量表的大小为 1; +- ⑤的时候局 age 变量的作用域结束; + +关于局部变量的作用域,《Effective Java》 中的第 57 条建议: + +>将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。 + +在此,我还有一点要提醒大家。为了尽可能节省栈帧耗用的内存空间,局部变量表中的槽是可以重用的,就像 `method()` 方法演示的那样,这就意味着,合理的作用域有助于提高程序的性能。 + +局部变量表的容量以槽(slot)为最小单位,一个槽可以容纳一个 32 位的数据类型(比如说 int,当然了,《Java 虚拟机规范》中没有明确指出一个槽应该占用的内存空间大小,但我认为这样更容易理解),像 float 和 double 这种明确占用 64 位的数据类型会占用两个紧挨着的槽。 + +来看下面的代码。 + +```java +public void solt() { + double d = 1.0; + int i = 1; +} +``` + +用 jclasslib 可以查看到,`solt()` 方法的 Maximum local variables 的值为 4。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-6734774b-376c-49bf-a915-508c7e829557.png) + +为什么等于 4 呢?带上 this 也就 3 个呀? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-91ad04f8-1620-44c9-83d1-6fbd7860701a.png) + +查看 LocalVaraiableTable 就明白了,变量 i 的下标为 3,也就意味着变量 d 占了两个槽。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-630b50e3-fc37-4748-8d20-852d5358f87a.png) + +#### 操作数栈 + +同局部变量表一样,操作数栈(Operand Stack)的最大深度也在编译的时候就确定了,被写入到了 Code 属性的 maximum stack size 中。当一个方法刚开始执行的时候,操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和取出数据,也就是入栈和出栈操作。 + +来看下面这段代码。 + +```java +public class OperandStack { + public void test() { + add(1,2); + } + + private int add(int a, int b) { + return a + b; + } +} +``` + +OperandStack 类共有 2 个方法,`test()` 方法中调用了 `add()` 方法,传递了 2 个参数。用 jclasslib 可以看到,`test()` 方法的 maximum stack size 的值为 3。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-f790aa0f-d742-465b-91bf-5f143ee098c1.png) + +这是因为调用成员方法的时候会将 this 和所有参数压入栈中,调用完毕后 this 和参数都会一一出栈。通过 「Bytecode」 面板可以查看到对应的字节码指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-c37add5c-a74b-4bd6-8c8e-9085d9e6d374.png) + +- aload_0 用于将局部变量表中下标为 0 的引用类型的变量,也就是 this 加载到操作数栈中; +- iconst_1 用于将整数 1 加载到操作数栈中; +- iconst_2 用于将整数 2 加载到操作数栈中; +- invokevirtual 用于调用对象的成员方法; +- pop 用于将栈顶的值出栈; +- return 为 void 方法的返回指令。 + +再来看一下 `add()` 方法的字节码指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-49e3f396-7ea8-49f5-81d4-093b9bdfa453.png) + +- iload_1 用于将局部变量表中下标为 1 的 int 类型变量加载到操作数栈上(下标为 0 的是 this); +- iload_2 用于将局部变量表中下标为 2 的 int 类型变量加载到操作数栈上; +- iadd 用于 int 类型的加法运算; +- ireturn 为返回值为 int 的方法返回指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-3ffdbe03-c0e4-49de-97a6-76666964a087.png) + +操作数中的数据类型必须与字节码指令匹配,以上面的 iadd 指令为例,该指令只能用于整型数据的加法运算,它在执行的时候,栈顶的两个数据必须是 int 类型的,不能出现一个 long 型和一个 double 型的数据进行 iadd 命令相加的情况。 + +#### 动态链接 + +每个栈帧都包含了一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。 + +来看下面这段代码。 + +```java +public class DynamicLinking { + static abstract class Human { + protected abstract void sayHello(); + } + + static class Man extends Human { + @Override + protected void sayHello() { + System.out.println("男人哭吧哭吧不是罪"); + } + } + + static class Woman extends Human { + @Override + protected void sayHello() { + System.out.println("山下的女人是老虎"); + } + } + + public static void main(String[] args) { + Human man = new Man(); + Human woman = new Woman(); + man.sayHello(); + woman.sayHello(); + man = new Woman(); + man.sayHello(); + } +} +``` + +大家对 Java 重写有了解的话,应该能看懂这段代码的意思。Man 类和 Woman 类继承了 Human 类,并且重写了 `sayHello()` 方法。来看一下运行结果: + +``` +男人哭吧哭吧不是罪 +山下的女人是老虎 +山下的女人是老虎 +``` + +这个运行结果很好理解,man 的引用类型为 Human,但指向的是 Man 对象,woman 的引用类型也为 Human,但指向的是 Woman 对象;之后,man 又指向了新的 Woman 对象。 + +从面向对象编程的角度,从多态的角度,我们对运行结果是很好理解的,但站在 Java 虚拟机的角度,它是如何判断 man 和 woman 该调用哪个方法的呢? + +用 jclasslib 看一下 main 方法的字节码指令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-93a21aaf-ff67-445d-8ddb-ac6f72fd9b25.png) + +- 第 1 行:new 指令创建了一个 Man 对象,并将对象的内存地址压入栈中。 +- 第 2 行:dup 指令将栈顶的值复制一份并压入栈顶。因为接下来的指令 invokespecial 会消耗掉一个当前类的引用,所以需要复制一份。 +- 第 3 行:invokespecial 指令用于调用构造方法进行初始化。 +- 第 4 行:astore_1,Java 虚拟机从栈顶弹出 Man 对象的引用,然后将其存入下标为 1 局部变量 man 中。 +- 第 5、6、7、8 行的指令和第 1、2、3、4 行类似,不同的是 Woman 对象。 +- 第 9 行:aload_1 指令将第局部变量 man 压入操作数栈中。 +- 第 10 行:invokevirtual 指令调用对象的成员方法 `sayHello()`,注意此时的对象类型为 `com/itwanger/jvm/DynamicLinking$Human`。 +- 第 11 行:aload_2 指令将第局部变量 woman 压入操作数栈中。 +- 第 12 行同第 10 行。 + +注意,从字节码的角度来看,`man.sayHello()`(第 10 行)和 `woman.sayHello()`(第 12 行)的字节码是完全相同的,但我们都知道,这两句指令最终执行的目标方法并不相同。 + +究竟发生了什么呢? + +还得从 `invokevirtual` 这个指令着手,看它是如何实现多态的。根据《Java 虚拟机规范》,invokevirtual 指令在运行时的解析过程可以分为以下几步: + +>①、找到操作数栈顶的元素所指向的对象的实际类型,记作 C。 +②、如果在类型 C 中找到与常量池中的描述符匹配的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;否则返回 `java.lang.IllegalAccessError` 异常。 +③、否则,按照继承关系从下往上一次对 C 的各个父类进行第二步的搜索和验证。 +④、如果始终没有找到合适的方法,则抛出 `java.lang.AbstractMethodError` 异常。 + +也就是说,invokevirtual 指令在第一步的时候就确定了运行时的实际类型,所以两次调用中的 invokevirtual 指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接受者的实际类型来选择方法版本,这个过程就是 Java 重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的过程称为**动态链接**。 + +#### 方法返回地址 + +当一个方法开始执行后,只有两种方式可以退出这个方法: + +- 正常退出,可能会有返回值传递给上层的方法调用者,方法是否有返回值以及返回值的类型根据方法返回的指令来决定,像之前提到的 ireturn 用于返回 int 类型,return 用于 void 方法;还有其他的一些,lreturn 用于 long 型,freturn 用于 float,dreturn 用于 double,areturn 用于引用类型。 + +- 异常退出,方法在执行的过程中遇到了异常,并且没有得到妥善的处理,这种情况下,是不会给它的上层调用者返回任何值的。 + +无论是哪种方式退出,在方法退出后,都必须返回到方法最初被调用时的位置,程序才能继续执行。一般来说,方法正常退出的时候,PC 计数器的值会作为返回地址,栈帧中很可能会保存这个计数器的值,异常退出时则不会。 + +方法退出的过程实际上等同于把当前栈帧出栈,因此接下来可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整 PC 计数器的值,找到下一条要执行的指令等。 + + +#### 附加信息 + +虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,成为栈帧信息。 + +### 二. 方法调用 + +方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。 + +在程序运行时,进行方法调用是最为普遍、频繁的操作。前面说过 Class 文件的编译过程是不包含传统编译中的连接步骤的,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在运行时内存布局中的入口地址(相当于之前说的直接引用)。这个特性给 Java 带来了更强大的动态扩展能力,但也使得 Java 方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。 + +#### 解析 + +所有方法调用中的目标方法在 Class 文件里都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。话句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。 + +Java 语言中符合「编译器可知,运行期不可变」这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或者别的方式重写其它版本,因此它们都适合在类加载阶段解析。 + +与之相应的是,在 Java 虚拟机里提供了 5 条方法调用字节码指令,分别是: + +* invokestatic:调用静态方法; +* invokespecial:调用实例构造器 方法、私有方法和父类方法; +* invokevirtual:调用所有虚方法; +* invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象; +* invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。 + +只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法 4 类,它们在加载的时候就会把符号引用解析为直接引用。这些方法可以称为非虚方法,与之相反,其它方法称为虚方法(final 方法除外)。 + +Java 中的非虚方法除了使用 invokestatic、invokespecial 调用的方法之外还有一种,就是被 final 修饰的方法。虽然 final 方法是使用 invokevirtual 指令来调用的,但是由于它无法被覆盖,没有其它版本,所以也无需对方法接受者进行多态选择,又或者说多态选择的结果肯定是唯一的。在 Java 语言规范中明确说明了 final 方法是一种非虚方法。 + +解析调用一定是个静态过程,在编译期间就能完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。而分派(Dispatch)调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派 4 种分派组合情况,下面我们再看看虚拟机中的方法分派是如何进行的。 + +#### 分派 + +面向对象有三个基本特征,封装、继承和多态。这里要说的分派将会揭示多态特征的一些最基本的体现,如「重载」和「重写」在 Java 虚拟机中是如何实现的?虚拟机是如何确定正确目标方法的? + +**静态分派** + +在开始介绍静态分派前我们先看一段代码。 + +```java +/** + * 方法静态分派演示 + * + * @author baronzhang + */ +public class StaticDispatch { + + private static abstract class Human { } + + private static class Man extends Human { } + + private static class Woman extends Human { } + + private void sayHello(Human guy) { + System.out.println("Hello, guy!"); + } + + private void sayHello(Man man) { + System.out.println("Hello, man!"); + } + + private void sayHello(Woman woman) { + System.out.println("Hello, woman!"); + } + + public static void main(String[] args) { + + Human man = new Man(); + Human woman = new Woman(); + StaticDispatch dispatch = new StaticDispatch(); + dispatch.sayHello(man); + dispatch.sayHello(woman); + } +} +``` + +运行后这段程序的输出结果如下: + +``` +Hello, guy! +Hello, guy! +``` + +稍有经验的 Java 程序员都能得出上述结论,但为什么我们传递给 sayHello() 方法的实际参数类型是 Man 和 Woman,虚拟机在执行程序时选择的却是 Human 的重载呢?要理解这个问题,我们先弄清两个概念。 + +```java +Human man = new Man(); +``` + +上面这段代码中的「Human」称为变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),后面的「Man」称为变量为实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅发生在使用时,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。 + +弄清了这两个概念,再来看 StaticDispatch 类中 main() 方法里的两次 sayHello() 调用,在方法接受者已经确定是对象「dispatch」的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中定义了两个静态类型相同但是实际类型不同的变量,但是虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此在编译阶段, Javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello(Human) 作为调用目标,并把这个方法的符号引用写到 man() 方法里的两条 invokevirtual 指令的参数中。 + +所有依赖静态类型来定位方法执行版本的分派动作称为**静态分派**。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。 + +另外,编译器虽然能确定方法的重载版本,但是很多情况下这个重载版本并不是「唯一」的,因此往往只能确定一个「更加合适」的版本。**产生这种情况的主要原因是字面量不需要定义,所以字面量没有显示的静态类型,它的静态类型只能通过语言上的规则去理解和推断**。下面的代码展示了什么叫「更加合适」的版本。 + +```java +/** + * @author baronzhang + */ +public class Overlaod { + + static void sayHello(Object arg) { + System.out.println("Hello, Object!"); + } + + static void sayHello(int arg) { + System.out.println("Hello, int!"); + } + + static void sayHello(long arg) { + System.out.println("Hello, long!"); + } + + static void sayHello(Character arg) { + System.out.println("Hello, Character!"); + } + + static void sayHello(char arg) { + System.out.println("Hello, char!"); + } + + static void sayHello(char... arg) { + System.out.println("Hello, char...!"); + } + + static void sayHello(Serializable arg) { + System.out.println("Hello, Serializable!"); + } + + public static void main(String[] args) { + sayHello('a'); + } +} +``` + +上面代码的运行结果为: + +``` +Hello, char! +``` + +这很好理解,‘a’ 是一个 char 类型的数据,自然会寻找参数类型为 char 的重载方法,如果注释掉 sayHello(chat arg) 方法,那么输出结果将会变为: + +``` +Hello, int! +``` + +这时发生了一次类型转换, ‘a’ 除了可以代表一个字符,还可以代表数字 97,因为字符 ‘a’ 的 Unicode 数值为十进制数字 97,因此参数类型为 int 的重载方法也是合适的。我们继续注释掉 sayHello(int arg) 方法,输出变为: + +``` +Hello, long! +``` + +这时发生了两次类型转换,‘a’ 转型为整数 97 之后,进一步转型为长整型 97L,匹配了参数类型为 long 的重载方法。我们继续注释掉 sayHello(long arg) 方法,输出变为: + +``` +Hello, Character! +``` + +这时发生了一次自动装箱, ‘a’ 被包装为它的封装类型 java.lang.Character,所以匹配到了类型为 Character 的重载方法,继续注释掉 sayHello(Character arg) 方法,输出变为: + +``` +Hello, Serializable! +``` + +这里输出之所以为「Hello, Serializable!」,是因为 java.lang.Serializable 是 java.lang.Character 类实现的一个接口,当自动装箱后发现还是找不到装箱类,但是找到了装箱类实现了的接口类型,所以紧接着又发生了一次自动转换。char 可以转型为 int,但是 Character 是绝对不会转型为 Integer 的,他只能安全的转型为它实现的接口或父类。Character 还实现了另外一个接口 java.lang.Comparable,如果同时出现两个参数分别为 Serializable 和 Comparable 的重载方法,那它们在此时的优先级是一样的。编译器无法确定要自动转型为哪种类型,会提示类型模糊,拒绝编译。程序必须在调用时显示的指定字面量的静态类型,如:sayHello((Comparable) 'a'),才能编译通过。继续注释掉 sayHello(Serializable arg) 方法,输出变为: + +``` +Hello, Object! +``` + +这时是 char 装箱后转型为父类了,如果有多个父类,那将在继承关系中从下往上开始搜索,越接近上层的优先级越低。即使方法调用的入参值为 null,这个规则依然适用。继续注释掉 sayHello(Serializable arg) 方法,输出变为: + +``` +Hello, char...! +``` + +7 个重载方法以及被注释得只剩一个了,可见变长参数的重载优先级是最低的,这时字符 ‘a’ 被当成了一个数组元素。 + +前面介绍的这一系列过程演示了编译期间选择静态分派目标的过程,这个过程也是 Java 语言实现方法重载的本质。 + +**动态分派** + +动态分派和多态性的另一个重要体现「重写(Override)」有着密切的关联,我们依旧通过代码来理解什么是动态分派。 + +```java +/** + * 方法动态分派演示 + * + * @author baronzhang + */ +public class DynamicDispatch { + + static abstract class Human { + + abstract void sayHello(); + } + + static class Man extends Human { + + @Override + void sayHello() { + System.out.println("Man say hello!"); + } + } + + static class Woman extends Human { + @Override + void sayHello() { + System.out.println("Woman say hello!"); + } + } + + public static void main(String[] args){ + + Human man = new Man(); + Human woman = new Woman(); + man.sayHello(); + woman.sayHello(); + + man = new Woman(); + man.sayHello(); + } +} +``` + +代码执行结果: + +``` +Man say hello! +Woman say hello! +Woman say hello! +``` + +对于上面的代码,虚拟机是如何确定要调用哪个方法的呢?显然这里不再通过静态类型来决定了,因为静态类型同样都是 Human 的两个变量 man 和 woman 在调用 sayHello() 方法时执行了不同的行为,并且变量 man 在两次调用中执行了不同的方法。导致这个结果的原因是因为它们的实际类型不同。对于虚拟机是如何通过实际类型来分派方法执行版本的,这里我们就不做介绍了,有兴趣的可以去看看原著。 + +我们把这种在运行期根据实际类型来确定方法执行版本的分派称为**动态分派**。 + +**单分派和多分派** + +方法的接收者和方法的参数统称为方法的宗量,这个定义最早来源于《Java 与模式》一书。根据分派基于多少宗量,可将分派划分为**单分派**和**多分派**。 + +单分派是根据一个宗量来确定方法的执行版本;多分派则是根据多余一个宗量来确定方法的执行版本。 + +我们依旧通过代码来理解(代码以著名的 3Q 大战作为背景): + +```java +/** + * 单分派、多分派演示 + * + * @author baronzhang + */ +public class Dispatch { + + static class QQ { } + + static class QiHu360 { } + + static class Father { + + public void hardChoice(QQ qq) { + System.out.println("Father choice QQ!"); + } + + public void hardChoice(QiHu360 qiHu360) { + System.out.println("Father choice 360!"); + } + } + + static class Son extends Father { + + @Override + public void hardChoice(QQ qq) { + System.out.println("Son choice QQ!"); + } + + @Override + public void hardChoice(QiHu360 qiHu360) { + System.out.println("Son choice 360!"); + } + } + + public static void main(String[] args) { + + Father father = new Father(); + Father son = new Son(); + + father.hardChoice(new QQ()); + son.hardChoice(new QiHu360()); + } +} +``` + +代码输出结果: + +``` +Father choice QQ! +Son choice 360! +``` + +我们先来看看编译阶段编译器的选择过程,也就是静态分派过程。这个时候选择目标方法的依据有两点:一是静态类型是 Father 还是 Son;二是方法入参是 QQ 还是 QiHu360。**因为是根据两个宗量进行选择的,所以 Java 语言的静态分派属于多分派**。 + +再看看运行阶段虚拟机的选择过程,也就是动态分派的过程。在执行 son.hardChoice(new QiHu360()) 时,由于编译期已经确定目标方法的签名必须为 hardChoice(QiHu360),这时参数的静态类型、实际类型都不会对方法的选择造成任何影响,唯一可以影响虚拟机选择的因数只有此方法的接收者的实际类型是 Father 还是 Son。因为只有一个宗量作为选择依据,所以 Java 语言的动态分派属于单分派。 + +综上所述,Java 语言是一门静态多分派、动态单分派的语言。 + +### 三. 基于栈的字节码解释执行引擎 + +虚拟机如何调用方法已经介绍完了,下面我们来看看虚拟机是如何执行方法中的字节码指令的。 + +#### 解释执行 + +Java 语言常被人们定义成「解释执行」的语言,但随着 JIT 以及可直接将 Java 代码编译成本地代码的编译器的出现,这种说法就不对了。只有确定了谈论对象是某种具体的 Java 实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切。 + +无论是解释执行还是编译执行,无论是物理机还是虚拟机,对于应用程序,机器都不可能像人一样阅读、理解,然后获得执行能力。大部分的程序代码到物理机的目标代码或者虚拟机执行的指令之前,都需要经过下图中的各个步骤。下图中最下面的那条分支,就是传统编译原理中程序代码到目标机器代码的生成过程;中间那条分支,则是解释执行的过程。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-3c8a0865-2a77-464e-8dd6-5616fd6a72d7.png) + + +如今,基于物理机、Java 虚拟机或者非 Java 的其它高级语言虚拟机的语言,大多都会遵循这种基于现代编译原理的思路,在执行前先对程序源代码进行词法分析和语法分析处理,把源代码转化为抽象语法树。对于一门具体语言的实现来说,词法分析、语法分析以至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是 C/C++。也可以为一个半独立的编译器,这类代表是 Java。又或者把这些步骤和执行全部封装在一个封闭的黑匣子中,如大多数的 JavaScript 执行器。 + +Java 语言中,Javac 编译器完成了程序代码经过词法分析、语法分析到抽象语法树、再遍历语法树生成字节码指令流的过程。因为这一部分动作是在 Java 虚拟机之外进行的,而解释器在虚拟机的内部,所以 Java 程序的编译就是半独立的实现。 + +许多 Java 虚拟机的执行引擎在执行 Java 代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。而对于最新的 Android 版本的执行模式则是 AOT + JIT + 解释执行,关于这方面我们后面有机会再聊。 + +#### 基于栈的指令集与基于寄存器的指令集 + +Java 编译器输出的指令流,基本上是一种基于栈的指令集架构。基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免的要受到硬件约束。栈架构的指令集还有一些其他优点,比如相对更加紧凑(字节码中每个字节就对应一条指令,而多地址指令集中还需要存放参数)、编译实现更加简单(不需要考虑空间分配的问题,所有空间都是在栈上操作)等。 + +栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。所有主流物理机的指令集都是寄存器架构也从侧面印证了这一点。 + +虽然栈架构指令集的代码非常紧凑,但是完成相同功能需要的指令集数量一般会比寄存器架构多,因为出栈、入栈操作本身就产生了相当多的指令数量。更重要的是,栈实现在内存中,频繁的栈访问也意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。由于指令数量和内存访问的原因,所以导致了栈架构指令集的执行速度会相对较慢。 + +正是基于上述原因,Android 虚拟机中采用了基于寄存器的指令集架构。不过有一点不同的是,前面说的是物理机上的寄存器,而 Android 上指的是虚拟机上的寄存器。 + +---- + + +引用链接:https://juejin.cn/post/6844903871010045960 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/how-run-java-code.md b/docs/jvm/how-run-java-code.md new file mode 100644 index 0000000000..ad5687b32f --- /dev/null +++ b/docs/jvm/how-run-java-code.md @@ -0,0 +1,178 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# JVM到底是如何运行Java代码的? + + +“二哥,看了上一篇 [Hello World](https://mp.weixin.qq.com/s/191I_2CVOxVuyfLVtb4jhg) 的程序后,我很好奇,它是怎么在 Run 面板里打印出‘三妹,少看手机少打游戏,好好学,美美哒’呢?”三妹咪了一口麦香可可奶茶后对我说。 + +“三妹,我们通常把 Java 分为编译期和运行时,弄清楚这两个阶段就知道原因了。” + +贴一下 HelloWorld 这段代码: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); + } +} +``` + +点击 IDEA 工具栏中的锤子按钮(Build Project,编译整个项目),如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-01.png) + + +这时候,就可以在 src 的同级目录 target 下找到一个名为 HelloWorld.class 的文件。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-02.png) + + +如果找不到的话,在目录上右键选择「Reload from Disk,从磁盘上重新加载」,如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-03.png) + + +可以双击打开它。 + +```java +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.itwanger.five; + +public class HelloWorld { + public HelloWorld() { + } + + public static void main(String[] args) { + System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); + } +} +``` + +IDEA 默认会用 Fernflower 反编译工具将字节码文件(后缀为 .class 的文件,也就是 Java 源代码编译后的文件)反编译为我们可以看得懂的 Java 源代码。但实际上,字节码文件并不是这样的,而是: + +``` +// class version 58.0 (58) +// access flags 0x21 +public class com/itwanger/five/HelloWorld { + + // compiled from: HelloWorld.java + + // access flags 0x1 + public ()V + L0 + LINENUMBER 6 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lcom/itwanger/five/HelloWorld; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x9 + public static main([Ljava/lang/String;)V + L0 + LINENUMBER 8 L0 + GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + LDC "\u4e09\u59b9\uff0c\u5c11\u770b\u624b\u673a\u5c11\u6253\u6e38\u620f\uff0c\u597d\u597d\u5b66\uff0c\u7f8e\u7f8e\u54d2\u3002" + INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + L1 + LINENUMBER 9 L1 + RETURN + L2 + LOCALVARIABLE args [Ljava/lang/String; L0 L2 0 + MAXSTACK = 2 + MAXLOCALS = 1 +} +``` + +是不是就有点懵逼了?新手看到这个很容易头大,不过不要担心,后面我再和大家一块深入研究一下,这里就是感受一下字节码的魅力。 + +那这个字节码文件是怎么看到的呢?可以通过 IDEA 菜单栏中的「View」→「Show Bytecode」查看,如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-04.png) + +PS:字节码并不是机器码,操作系统无法直接识别,需要在操作系统上安装不同版本的 Java 虚拟机(JVM)来识别。通常情况下,我们只需要安装不同版本的 JDK(Java Development Kit,Java 开发工具包)就行了,它里面包含了 JRE(Java Runtime Environment,Java 运行时环境),而 JRE 又包含了 JVM。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-05.png) + +Windows、Linux、MacOS 等操作系统都有相应的 JDK,只要安装好了 JDK 就有了 Java 语言的运行时环境,就可以把一份字节码文件在不同的平台上运行了。可以在 [Oracle 官网](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html)上下载不同版本的 JDK。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-06.png) + +PPS:为什么要查看字节码呢?查看字节码文件更容易让我们搞懂 Java 代码背后的原理,比如搞懂 Java 中的各种语法糖的本质。 + +相比于 IDEA 自带的「Show Bytecode」功能,我更推荐 `jclasslib` 这款插件,可以在插件市场中安装(我已经安装过了)。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-07.png) + +安装完成之后,点击 View -> Show Bytecode With jclasslib 即可通过 jclasslib 查看字节码文件了(点击之前,光标要停留在对应的类文件上),如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-08.png) + +使用 jclasslib 不仅可以直观地查看类对应的字节码文件,还可以查看类的基本信息、常量池、接口、字段、方法等信息,如下图所示。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-09.png) + + + +也就是说,在编译阶段,Java 会将 Java 源代码文件编译为字节码文件。在这个阶段,编译器会进行一些检查工作,比如说,某个关键字是不是写错了,语法上是不是符合预期了,不能有很明显的错误,否则带到运行时再检查出来就会比较麻烦了。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/overview/five-10.png) + +Java 字节码是沟通 JVM 与 Java 代码的桥梁,下面使用 javap 来稍微看一下字节码到底长什么样子。 + +``` +0 getstatic #2 +3 ldc #3 +5 invokevirtual #4 +8 return +``` + +Java 虚拟机采用基于栈的架构,其指令由操作码和操作数组成。这些[字节码指令](https://tobebetterjavaer.com/jvm/zijiema-zhiling.html),就叫作 opcode。其中,getstatic、ldc、invokevirtual、return 等,就是 opcode,可以看到是比较容易理解的。 + +我们继续使用 hexdump 看一下字节码的二进制内容。与以上字节码对应的二进制,就是下面这几个数字: + +``` +b2 00 02 12 03 b6 00 04 b1 +``` + +> 注意:这里是二进制文件的16进制表示,也就是hex,一般分析二进制文件都是以hex进行分析。 + +我们可以看一下它们的对应关系。 + +``` +0xb2 getstatic 获取静态字段的值 +0x12 ldc 常量池中的常量值入栈 +0xb6 invokevirtual 运行时方法绑定调用方法 +0xb1 return void 函数返回 +``` + +opcode 有一个字节的长度(0~255),意味着指令集的操作码个数不能操作 256 条。而紧跟在 opcode 后面的是被操作数。比如 b2 00 02,就代表了 `getstatic #2 `。 + +JVM 就是靠解析这些 opcode 和操作数来完成程序的执行的。当我们使用 Java 命令运行 .class 文件的时候,实际上就相当于启动了一个 JVM 进程。 + +然后 JVM 会翻译这些字节码,它有两种执行方式。常见的就是解释执行,将 opcode + 操作数翻译成机器代码;另外一种执行方式就是 [JIT](https://tobebetterjavaer.com/jvm/jit.html),也就是我们常说的即时编译,它会在一定条件下将字节码编译成机器码之后再执行。 + +这些 .class 文件会被加载、存放到 metaspace 中,等待被调用,这里会有一个[类加载器](https://tobebetterjavaer.com/jvm/class-load.html)的概念。 + +而 JVM 的程序运行,都是在栈上完成的,这和其他普通程序的执行是类似的,同样分为堆和栈。比如我们现在运行到了 main 方法,就会给它分配一个栈帧。当退出方法体时,会弹出相应的栈帧。你会发现,大多数字节码指令,就是不断的对栈帧进行操作。 + +而其他大块数据,是存放在堆上的。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/how-run-java-code-91dac706-1c4e-4775-bc4e-b2104283aa04.png) + + diff --git a/docs/src/jvm/hsdb.md b/docs/jvm/hsdb.md similarity index 75% rename from docs/src/jvm/hsdb.md rename to docs/jvm/hsdb.md index 06eccf8cec..fb8eb14390 100644 --- a/docs/src/jvm/hsdb.md +++ b/docs/jvm/hsdb.md @@ -1,17 +1,12 @@ --- -title: 如何调试 JVM 运行时数据?HSDB(Hotspot Debugger)从入门到实战 -shortTitle: 如何调试 JVM 运行时数据? category: - Java核心 + - JVM tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,HSDB(Hotspot Debugger)从入门到实战 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,HSDB + - Java --- +# HSDB(Hotspot Debugger)从入门到实战 `HSDB(Hotspot Debugger)`,是一款内置于 SA 中的 GUI 调试工具,可用于调试 JVM 运行时数据,从而进行故障排除。 @@ -77,33 +72,33 @@ $ jps ### 可视化线程栈 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-a606c6ac-1cfc-44c3-8fdf-e0eeeabbf05a.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-a606c6ac-1cfc-44c3-8fdf-e0eeeabbf05a.png) ### 对象直方图 `Tools -> Object Histogram`,我们可以通过对象直方图快速定位某个类型的对象的地址以供我们进一步分析 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-cf39737a-7d6a-42de-b843-123cba1f96aa.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-cf39737a-7d6a-42de-b843-123cba1f96aa.png) -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-974d211c-e627-40d6-af13-9560ebae0bfa.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-974d211c-e627-40d6-af13-9560ebae0bfa.png) ### OOP信息 我们可以根据对象地址在 `Tools -> Inspector` 获取对象的在 JVM 层的实例 `instanceOopDesc` 对象,它包括对象头 `_mark` 和 `_metadata` 以及实例信息 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-026fb881-59a2-4e0f-ac4a-a2a7b505a707.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-026fb881-59a2-4e0f-ac4a-a2a7b505a707.png) ### 堆信息 我们可以通过 `Tools -> Heap Parameters` 获取堆信息,可以结合对象地址判断对象位置 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-3baabaf0-1681-4443-b8db-ae08128744d6.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-3baabaf0-1681-4443-b8db-ae08128744d6.png) ### 加载类列表 我们可以通过 `Tools -> Class Browser` 来获取所有加载类列表 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-7e4ebd1f-ba9c-4862-b4c3-574de5c30d6b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-7e4ebd1f-ba9c-4862-b4c3-574de5c30d6b.png) ### 元数据区 @@ -113,18 +108,18 @@ HotSpot VM 里有一套对象专门用来存放元数据,它们包括:  * `ConstantPool/ConstantPoolCache` 对象:每个 `InstanceKlass` 关联着一个 `ConstantPool`,作为该类型的运行时常量池。这个常量池的结构跟 Class 文件里的常量池基本上是对应的 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-07d80d18-be4e-4861-bea3-291eea0ff262.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-07d80d18-be4e-4861-bea3-291eea0ff262.png) -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-b7b0ebed-cd38-42b7-bebc-41090409a1db.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-b7b0ebed-cd38-42b7-bebc-41090409a1db.png) * `Method` 对象,用来描述 Java 方法的总体信息,如方法入口地址、调用/循环计数器等等 * `ConstMethod` 对象,记录着 Java 方法的不变的描述信息,包括方法名、方法的访问修饰符、**字节码**、行号表、局部变量表等等。**注意,字节码指令被分配在 `constMethodOop` 对象的内存区域的末尾** * `MethodData` 对象,记录着 Java 方法执行时的 profile 信息,例如某方法里的某个字节码之类是否从来没遇到过 null,某个条件跳转是否总是走同一个分支,等等。这些信息在解释器(多层编译模式下也在低层的编译生成的代码里)收集,然后供给 HotSpot Server Compiler 用于做激进优化。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-59231922-9ce3-4107-ab1a-b33818cbab96.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-59231922-9ce3-4107-ab1a-b33818cbab96.png) -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-85c6fca7-2d1f-4194-bc07-fd1e2ab18632.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-85c6fca7-2d1f-4194-bc07-fd1e2ab18632.png) * `Symbol` 对象,对应 Class 文件常量池里的 `JVM_CONSTANT_Utf8` 类型的常量。有一个 VM 全局的 `SymbolTable` 管理着所有 `Symbol`。`Symbol` 由所有 Java 类所共享。 @@ -164,23 +159,23 @@ class VMShow { 首先查看对象直方图可以找到三个 VMShow 对象 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-e5f0f200-83fd-4529-b5d6-416f9a6f626b.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-e5f0f200-83fd-4529-b5d6-416f9a6f626b.png) 那么如何确定这三个地址分别属于哪些变量呢?首先找静态变量,它在 JDK8 中是在 Class 对象中的,因此我们可以找它们的反向指针,如果是`java.lang.Class` 的那么就是静态变量 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-301ebfc3-c2c4-49de-946a-5d2f1660e669.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-301ebfc3-c2c4-49de-946a-5d2f1660e669.png) 我们可以从 ObjTest 的 `instanceKlass` 中的镜像找到 class 对象来验证是否是该对象的 class -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-f22359ab-b066-405f-a754-197ccbd36884.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-f22359ab-b066-405f-a754-197ccbd36884.png) 那么成员变量和局部变量如何区分呢?成员变量会被类实例引用,而局部变量地址则在会被被放在栈区 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-51cb7321-fb7a-42b0-9321-edd410e3d328.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-51cb7321-fb7a-42b0-9321-edd410e3d328.png) 那么局部变量的反向指针都是 null,怎么确定它就被栈区所引用呢?我们可以看可视化线程栈 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-51fb09f4-06d7-4518-8038-5dd69d765862.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-51fb09f4-06d7-4518-8038-5dd69d765862.png) ### 分析字符串字面量存储区域 @@ -203,7 +198,7 @@ public class StringTest { 2. 打开对象直方图发现只有 1 个 `a` 的字符串对象 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-b5631e70-be3a-48de-99be-4468632d23e0.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-b5631e70-be3a-48de-99be-4468632d23e0.png) 3. 查找 StringTable 中 `a` 的对象地址 @@ -213,7 +208,7 @@ jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUti 可以根据需要改变 `matches` 中的值来匹配 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-76084b8d-6134-4866-8891-c24a43a3b836.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-76084b8d-6134-4866-8891-c24a43a3b836.png) 可以看到这个对象地址就是 StringTable 中引用的地址 @@ -221,7 +216,7 @@ jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUti 5. 重新使用对象直方图查看 String 值 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-f3c35027-2fe1-4b18-8a1c-b7243a9b5149.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-f3c35027-2fe1-4b18-8a1c-b7243a9b5149.png) 这里有5个值,`ab` 有3个: @@ -236,13 +231,13 @@ jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUti ``` -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-f07a9fc8-5744-4859-9df9-c3a1016e936a.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-f07a9fc8-5744-4859-9df9-c3a1016e936a.png) 那么运行时常量池中存放的是哪些呢?实际上它和 StringTable 一样是这些对象的引用,只不过 StringTable 是全局共享的,而运行时常量池只有该类的一些字面量。我们通过加载类列表可以查看 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-25fa5e06-0250-424b-8b05-aea770e80963.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-25fa5e06-0250-424b-8b05-aea770e80963.png) -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-88e8c10d-e09c-4a74-95e6-e5b21577459f.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-88e8c10d-e09c-4a74-95e6-e5b21577459f.png) ### 分析String.intern @@ -333,30 +328,24 @@ public class StringInternTest { jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUtilities.stringOopToString(s).matches('^(he|llo|hello|1|2|12)')) {print(s + ': ');s.printValueOn(java.lang.System.out); println('')}})" ``` -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-ca5d06a0-1690-4a8f-98cb-aa6bd7800afe.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-ca5d06a0-1690-4a8f-98cb-aa6bd7800afe.png) 但是 `hello` 对象还是存在的(new) -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-85253e10-d0c2-442b-a26b-91b1b2588f1c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-85253e10-d0c2-442b-a26b-91b1b2588f1c.png) 接着执行 s1.intern 会将 `hello` 对象的地址放入 StringTable -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-801d35f2-0c8a-4699-afbf-057c1e6cac6c.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-801d35f2-0c8a-4699-afbf-057c1e6cac6c.png) 再执行 `String s2="hello";` 会发现 `hello` 对象仍然只有一个,都指向同一个。 而继续在 6 打断点,即执行完 `String s4 = "12";`,因为 `12` 不在字符串常量池,那么会新建一个 `12`的实例,并让字符串常量池引用它,这样会发现就有两个 `12` 了 -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-47a42bfa-7645-4c9b-bcd8-aeabea1ae44f.png) - - -参考链接:https://juejin.cn/post/7072344870374866951 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/hsdb-47a42bfa-7645-4c9b-bcd8-aeabea1ae44f.png) +--- -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +>参考链接:https://zzcoder.cn/2019/12/06/HSDB%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E5%AE%9E%E6%88%98/ -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/jit.md b/docs/jvm/jit.md new file mode 100644 index 0000000000..afdbe868d3 --- /dev/null +++ b/docs/jvm/jit.md @@ -0,0 +1,756 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# Java即时编译(JIT)器原理解析及实践 + + +## 一、导读 + +常见的编译型语言如C++,通常会把代码直接编译成CPU所能理解的机器码来运行。而Java为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由javac编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java通常不如C++这类编译型语言。 + +为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。 + +即时编译器极大地提高了Java程序的运行速度,而且跟静态编译相比,即时编译器可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。目前,即时编译器已经非常成熟了,在性能层面甚至可以和编译型语言相比。不过在这个领域,大家依然在不断探索如何结合不同的编译方式,使用更加智能的手段来提升程序的运行速度。 + +## 二、Java的执行过程 + +Java的执行过程整体可以分为两个部分,第一步由javac将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析,编译原理中这部分的编译称为前端编译。接下来无需编译直接逐条将字节码解释执行,在解释执行的过程中,虚拟机同时对程序运行的信息进行收集,在这些信息的基础上,编译器会逐渐发挥作用,它会进行后端编译——把字节码编译成机器码,但不是所有的代码都会被编译,只有被JVM认定为的热点代码,才可能被编译。 + +怎么样才会被认为是热点代码呢?JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,存入codeCache中。当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能。整体的执行过程大致如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-9a62fc02-1a6a-451e-bb2b-19fc086d5be0.png) + +### 1\. JVM中的编译器 + +JVM中集成了两种编译器,Client Compiler和Server Compiler,它们的作用也不同。Client Compiler注重启动速度和局部的优化,Server Compiler则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢。两种编译器有着不同的应用场景,在虚拟机中同时发挥作用。 + +**Client Compiler** + +HotSpot VM带有一个Client Compiler C1编译器。这种编译器启动速度快,但是性能比较Server Compiler来说会差一些。C1会做三件事: + +* 局部简单可靠的优化,比如字节码上进行的一些基础优化,方法内联、常量传播等,放弃许多耗时较长的全局优化。 +* 将字节码构造成高级中间表示(High-level Intermediate Representation,以下称为HIR),HIR与平台无关,通常采用图结构,更适合JVM对程序进行优化。 +* 最后将HIR转换成低级中间表示(Low-level Intermediate Representation,以下称为LIR),在LIR的基础上会进行寄存器分配、窥孔优化(局部的优化方式,编译器在一个基本块或者多个基本块中,针对已经生成的代码,结合CPU自己指令的特点,通过一些认为可能带来性能提升的转换规则或者通过整体的分析,进行指令转换,来提升代码性能)等操作,最终生成机器码。 + +**Server Compiler** + +Server Compiler主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长,适用于长时间运行的后台程序,它的性能通常比Client Compiler高30%以上。目前,Hotspot虚拟机中使用的Server Compiler有两种:C2和Graal。 + +**C2 Compiler** + +在Hotspot VM中,默认的Server Compiler是C2编译器。 + +C2编译器在进行编译优化时,会使用一种控制流与数据流结合的图数据结构,称为Ideal Graph。 Ideal Graph表示当前程序的数据流向和指令间的依赖关系,依靠这种图结构,某些优化步骤(尤其是涉及浮动代码块的那些优化步骤)变得不那么复杂。 + +Ideal Graph的构建是在解析字节码的时候,根据字节码中的指令向一个空的Graph中添加节点,Graph中的节点通常对应一个指令块,每个指令块包含多条相关联的指令,JVM会利用一些优化技术对这些指令进行优化,比如Global Value Numbering、常量折叠等,解析结束后,还会进行一些死代码剔除的操作。生成Ideal Graph后,会在这个基础上结合收集的程序运行信息来进行一些全局的优化,这个阶段如果JVM判断此时没有全局优化的必要,就会跳过这部分优化。 + +无论是否进行全局优化,Ideal Graph都会被转化为一种更接近机器层面的MachNode Graph,最后编译的机器码就是从MachNode Graph中得的,生成机器码前还会有一些包括寄存器分配、窥孔优化等操作。关于Ideal Graph和各种全局的优化手段会在后面的章节详细介绍。Server Compiler编译优化的过程如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-f4d1b763-be02-4bb2-ab0e-45b1f0eb9550.png) + +**Graal Compiler** + +从JDK 9开始,Hotspot VM中集成了一种新的Server Compiler,Graal编译器。相比C2编译器,Graal有这样几种关键特性: + +* 前文有提到,JVM会在解释执行的时候收集程序运行的各种信息,然后编译器会根据这些信息进行一些基于预测的激进优化,比如分支预测,根据程序不同分支的运行概率,选择性地编译一些概率较大的分支。Graal比C2更加青睐这种优化,所以Graal的峰值性能通常要比C2更好。 +* 使用Java编写,对于Java语言,尤其是新特性,比如Lambda、Stream等更加友好。 +* 更深层次的优化,比如虚函数的内联、部分逃逸分析等。 + +Graal编译器可以通过Java虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler启用。当启用时,它将替换掉HotSpot中的C2编译器,并响应原本由C2负责的编译请求。 + +### 2\. 分层编译 + +在Java 7以前,需要研发人员根据服务的性质去选择编译器。对于需要快速启动的,或者一些不会长期运行的服务,可以采用编译效率较高的C1,对应参数-client。长期运行的服务,或者对峰值性能有要求的后台服务,可以采用峰值性能更好的C2,对应参数-server。Java 7开始引入了分层编译的概念,它结合了C1和C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次。五个层级分别是: + +1. 解释执行。 +2. 执行不带profiling的C1代码。 +3. 执行仅带方法调用次数以及循环回边执行次数profiling的C1代码。 +4. 执行带所有profiling的C1代码。 +5. 执行C2代码。 + +profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。 + +通常情况下,C2代码的执行效率要比C1代码的高出30%以上。C1层执行的代码,按执行效率排序从高至低则是1层>2层>3层。这5个层次中,1层和4层都是终止状态,当一个方法到达终止状态后,只要编译后的代码并没有失效,那么JVM就不会再次发出该方法的编译请求的。服务实际运行时,JVM会根据服务运行情况,从解释执行开始,选择不同的编译路径,直到到达终止状态。下图中就列举了几种常见的编译路径: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-a6cebc82-ed4d-4b6d-892a-c5b245d227ab.png) + +* 图中第①条路径,代表编译的一般情况,热点方法从解释执行到被3层的C1编译,最后被4层的C2编译。 +* 如果方法比较小(比如Java服务中常见的getter/setter方法),3层的profiling没有收集到有价值的数据,JVM就会断定该方法对于C1代码和C2代码的执行效率相同,就会执行图中第②条路径。在这种情况下,JVM会在3层编译之后,放弃进入C2编译,直接选择用1层的C1编译运行。 +* 在C1忙碌的情况下,执行图中第③条路径,在解释执行过程中对程序进行profiling ,根据信息直接由第4层的C2编译。 +* 前文提到C1中的执行效率是1层>2层>3层,第3层一般要比第2层慢35%以上,所以在C2忙碌的情况下,执行图中第④条路径。这时方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间。 +* 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第⑤条执行路径代表的就是反优化。 + +总的来说,C1的编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始,JVM默认开启分层编译。 + +### 3\. 即时编译的触发 + +Java虚拟机根据方法的调用次数以及循环回边的执行次数来触发即时编译。循环回边是一个控制流图中的概念,程序中可以简单理解为往回跳转的指令,比如下面这段代码: + +循环回边 + +```java +public void nlp(Object obj) { + int sum = 0; + for (int i = 0; i < 200; i++) { + sum += i; + } +} + +``` + +上面这段代码经过编译生成下面的字节码。其中,偏移量为18的字节码将往回跳至偏移量为4的字节码中。在解释执行时,每当运行一次该指令,Java虚拟机便会将该方法的循环回边计数器加1。 + +字节码 + +``` +public void nlp(java.lang.Object); + Code: + 0: iconst_0 + 1: istore_1 + 2: iconst_0 + 3: istore_2 + 4: iload_2 + 5: sipush 200 + 8: if_icmpge 21 + 11: iload_1 + 12: iload_2 + 13: iadd + 14: istore_1 + 15: iinc 2, 1 + 18: goto 4 + 21: return + +``` + +在即时编译过程中,编译器会识别循环的头部和尾部。上面这段字节码中,循环体的头部和尾部分别为偏移量为11的字节码和偏移量为15的字节码。编译器将在循环体结尾增加循环回边计数器的代码,来对循环进行计数。 + +当方法的调用次数和循环回边的次数的和,超过由参数-XX:CompileThreshold指定的阈值时(使用C1时,默认值为1500;使用C2时,默认值为10000),就会触发即时编译。 + +开启分层编译的情况下,-XX:CompileThreshold参数设置的阈值将会失效,触发编译会由以下的条件来判断: + +* 方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数。 +* 方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时。 + +分层编译触发条件公式 + +``` +i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s && i + b > TierXCompileThreshold * s) +i为调用次数,b是循环回边次数 + +``` + +上述满足其中一个条件就会触发即时编译,并且JVM会根据当前的编译方法数以及编译线程数动态调整系数s。 + +## 三、编译优化 + +即时编译器会对正在运行的服务进行一系列的优化,包括字节码解析过程中的分析,根据编译过程中代码的一些中间形式来做局部优化,还会根据程序依赖图进行全局优化,最后才会生成机器码。 + +### 1\. 中间表达形式(Intermediate Representation) + +在编译原理中,通常把编译器分为前端和后端,前端编译经过词法分析、语法分析、语义分析生成中间表达形式(Intermediate Representation,以下称为IR),后端会对IR进行优化,生成目标代码。 + +Java字节码就是一种IR,但是字节码的结构复杂,字节码这样代码形式的IR也不适合做全局的分析优化。现代编译器一般采用图结构的IR,静态单赋值(Static Single Assignment,SSA)IR是目前比较常用的一种。这种IR的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用。举个例子: + +SSA IR + +``` +Plain Text +{ + a = 1; + a = 2; + b = a; +} + +``` + +上述代码中我们可以轻易地发现a = 1的赋值是冗余的,但是编译器不能。传统的编译器需要借助数据流分析,从后至前依次确认哪些变量的值被覆盖掉。不过,如果借助了SSA IR,编译器则可以很容易识别冗余赋值。 + +上面代码的SSA IR形式的伪代码可以表示为: + +SSA IR + +``` +Plain Text +{ + a_1 = 1; + a_2 = 2; + b_1 = a_2; +} + +``` + +由于SSA IR中每个变量只能赋值一次,所以代码中的a在SSA IR中会分成a_1、a_2两个变量来赋值,这样编译器就可以很容易通过扫描这些变量来发现a_1的赋值后并没有使用,赋值是冗余的。 + +除此之外,SSA IR对其他优化方式也有很大的帮助,例如下面这个死代码删除(Dead Code Elimination)的例子: + +DeadCodeElimination + +```java +public void DeadCodeElimination{ + int a = 2; + int b = 0 + if(2 > 1){ + a = 1; + } else{ + b = 2; + } + add(a,b) +} + +``` + +可以得到SSA IR伪代码: + +DeadCodeElimination + +``` +a_1 = 2; +b_1 = 0 +if true: + a_2 = 1; +else + b_2 = 2; +add(a,b) + +``` + +编译器通过执行字节码可以发现 b_2 赋值后不会被使用,else分支不会被执行。经过死代码删除后就可以得到代码: + +DeadCodeElimination + +```java +public void DeadCodeElimination{ + int a = 1; + int b = 0; + add(a,b) +} + +``` + +我们可以将编译器的每一种优化看成一个图优化算法,它接收一个IR图,并输出经过转换后的IR图。编译器优化的过程就是一个个图节点的优化串联起来的。 + +**C1中的中间表达形式** + +前文提及C1编译器内部使用高级中间表达形式HIR,低级中间表达形式LIR来进行各种优化,这两种IR都是SSA形式的。 + +HIR是由很多基本块(Basic Block)组成的控制流图结构,每个块包含很多SSA形式的指令。基本块的结构如下图所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-037b406d-1040-4bf8-976c-abf14a92402d.png) + +其中,predecessors表示前驱基本块(由于前驱可能是多个,所以是BlockList结构,是多个BlockBegin组成的可扩容数组)。同样,successors表示多个后继基本块BlockEnd。除了这两部分就是主体块,里面包含程序执行的指令和一个next指针,指向下一个执行的主体块。 + +从字节码到HIR的构造最终调用的是GraphBuilder,GraphBuilder会遍历字节码构造所有代码基本块储存为一个链表结构,但是这个时候的基本块只有BlockBegin,不包括具体的指令。第二步GraphBuilder会用一个ValueStack作为操作数栈和局部变量表,模拟执行字节码,构造出对应的HIR,填充之前空的基本块,这里给出简单字节码块构造HIR的过程示例,如下所示: + +字节码构造HIR + +``` + 字节码 Local Value operand stack HIR + 5: iload_1 [i1,i2] [i1] + 6: iload_2 [i1,i2] [i1,i2] + ................................................ i3: i1 * i2 + 7: imul + 8: istore_3 [i1,i2,i3] [i3] + +``` + +可以看出,当执行iload_1时,操作数栈压入变量i1,执行iload_2时,操作数栈压入变量i2,执行相乘指令imul时弹出栈顶两个值,构造出HIR i3 : i1 * i2,生成的i3入栈。 + +C1编译器优化大部分都是在HIR之上完成的。当优化完成之后它会将HIR转化为LIR,LIR和HIR类似,也是一种编译器内部用到的IR,HIR通过优化消除一些中间节点就可以生成LIR,形式上更加简化。 + +**Sea-of-Nodes IR** + +C2编译器中的Ideal Graph采用的是一种名为Sea-of-Nodes中间表达形式,同样也是SSA形式的。它最大特点是去除了变量的概念,直接采用值来进行运算。为了方便理解,可以利用IR可视化工具Ideal Graph Visualizer(IGV),来展示具体的IR图。比如下面这段代码: + +example + +```java +public static int foo(int count) { + int sum = 0; + for (int i = 0; i < count; i++) { + sum += i; + } + return sum; +} + +``` + +对应的IR图如下所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-f96da42a-568b-45ba-bed1-f4238ac32e14.png) + +图中若干个顺序执行的节点将被包含在同一个基本块之中,如图中的B0、B1等。B0基本块中0号Start节点是方法入口,B3中21号Return节点是方法出口。红色加粗线条为控制流,蓝色线条为数据流,而其他颜色的线条则是特殊的控制流或数据流。被控制流边所连接的是固定节点,其他的则是浮动节点(浮动节点指只要能满足数据依赖关系,可以放在不同位置的节点,浮动节点变动的这个过程称为Schedule)。 + +这种图具有轻量级的边结构。 图中的边仅由指向另一个节点的指针表示。节点是Node子类的实例,带有指定输入边的指针数组。这种表示的优点是改变节点的输入边很快,如果想要改变输入边,只要将指针指向Node,然后存入Node的指针数组就可以了。 + +依赖于这种图结构,通过收集程序运行的信息,JVM可以通过Schedule那些浮动节点,从而获得最好的编译效果。 + +**Phi And Region Nodes** + +Ideal Graph是SSA IR。 由于没有变量的概念,这会带来一个问题,就是不同执行路径可能会对同一变量设置不同的值。例如下面这段代码if语句的两个分支中,分别返回5和6。此时,根据不同的执行路径,所读取到的值很有可能不同。 + +example + +```java +int test(int x) { +int a = 0; + if(x == 1) { + a = 5; + } else { + a = 6; + } + return a; +} + +``` + +为了解决这个问题,就引入一个Phi Nodes的概念,能够根据不同的执行路径选择不同的值。于是,上面这段代码可以表示为下面这张图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-fb8b2bac-a7b9-45eb-bd28-05e35cf043ae.png) + +Phi Nodes中保存不同路径上包含的所有值,Region Nodes根据不同路径的判断条件,从Phi Nodes取得当前执行路径中变量应该赋予的值,带有Phi节点的SSA形式的伪代码如下: + +Phi Nodes + +``` +int test(int x) { + a_1 = 0; + if(x == 1){ + a_2 = 5; + }else { + a_3 = 6; + } + a_4 = Phi(a_2,a_3); + return a_4; +} + +``` + +**Global Value Numbering** + +Global Value Numbering(GVN) 是一种因为Sea-of-Nodes变得非常容易的优化技术 。 + +GVN是指为每一个计算得到的值分配一个独一无二的编号,然后遍历指令寻找优化的机会,它可以发现并消除等价计算的优化技术。如果一段程序中出现了多次操作数相同的乘法,那么即时编译器可以将这些乘法合并为一个,从而降低输出机器码的大小。如果这些乘法出现在同一执行路径上,那么GVN还将省下冗余的乘法操作。在Sea-of-Nodes中,由于只存在值的概念,因此GVN算法将非常简单:即时编译器只需判断该浮动节点是否与已存在的浮动节点的编号相同,所输入的IR节点是否一致,便可以将这两个浮动节点归并成一个。比如下面这段代码: + +GVN + +``` +a = 1; +b = 2; +c = a + b; +d = a + b; +e = d; + +``` + +GVN会利用Hash算法编号,计算a = 1时,得到编号1,计算b = 2时得到编号2,计算c = a + b时得到编号3,这些编号都会放入Hash表中保存,在计算d = a + b时,会发现a + b已经存在Hash表中,就不会再进行计算,直接从Hash表中取出计算过的值。最后的e = d也可以由Hash表中查到而进行复用。 + +可以将GVN理解为在IR图上的公共子表达式消除(Common Subexpression Elimination,CSE)。两者区别在于,GVN直接比较值的相同与否,而CSE是借助词法分析器来判断两个表达式相同与否。 + +### 2.方法内联 + +方法内联,是指在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。JIT大部分的优化都是在内联的基础上进行的,方法内联是即时编译器中非常重要的一环。 + +Java服务中存在大量getter/setter方法,如果没有方法内联,在调用getter/setter时,程序执行时需要保存当前方法的执行位置,创建并压入用于getter/setter的栈帧、访问字段、弹出栈帧,最后再恢复当前方法的执行。内联了对 getter/setter的方法调用后,上述操作仅剩字段访问。在C2编译器 中,方法内联在解析字节码的过程中完成。当遇到方法调用字节码时,编译器将根据一些阈值参数决定是否需要内联当前方法的调用。如果需要内联,则开始解析目标方法的字节码。比如下面这个示例(来源于网络): + +方法内联的过程 + +```java +public static boolean flag = true; +public static int value0 = 0; +public static int value1 = 1; +​ +public static int foo(int value) { + int result = bar(flag); + if (result != 0) { + return result; + } else { + return value; + } +} +​ +public static int bar(boolean flag) { + return flag ? value0 : value1; +} + +``` + +bar方法的IR图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-04ca4a7e-46e7-4782-bb43-333aea31ed57.png) + +内联后的IR图: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-4bf4d190-7fd2-4542-b948-0c85ee6963d2.png) + +内联不仅将被调用方法的IR图节点复制到调用者方法的IR图中,还要完成其他操作。 + +被调用方法的参数替换为调用者方法进行方法调用时所传入参数。上面例子中,将bar方法中的1号P(0)节点替换为foo方法3号LoadField节点。 + +调用者方法的IR图中,方法调用节点的数据依赖会变成被调用方法的返回。如果存在多个返回节点,会生成一个Phi节点,将这些返回值聚合起来,并作为原方法调用节点的替换对象。图中就是将8号==节点,以及12号Return节点连接到原5号Invoke节点的边,然后指向新生成的24号Phi节点中。 + +如果被调用方法将抛出某种类型的异常,而调用者方法恰好有该异常类型的处理器,并且该异常处理器覆盖这一方法调用,那么即时编译器需要将被调用方法抛出异常的路径,与调用者方法的异常处理器相连接。 + +**方法内联的条件** + +编译器的大部分优化都是在方法内联的基础上。所以一般来说,内联的方法越多,生成代码的执行效率越高。但是对于即时编译器来说,内联的方法越多,编译时间也就越长,程序达到峰值性能的时刻也就比较晚。 + +可以通过虚拟机参数-XX:MaxInlineLevel调整内联的层数,以及1层的直接递归调用(可以通过虚拟机参数-XX:MaxRecursiveInlineLevel调整)。一些常见的内联相关的参数如下表所示: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-48e4ff65-07ec-487e-8b08-2f8fed1e56bd.png) + +**虚函数内联** + +内联是JIT提升性能的主要手段,但是虚函数使得内联是很难的,因为在内联阶段并不知道他们会调用哪个方法。例如,我们有一个数据处理的接口,这个接口中的一个方法有三种实现add、sub和multi,JVM是通过保存虚函数表Virtual Method Table(以下称为VMT)存储class对象中所有的虚函数,class的实例对象保存着一个VMT的指针,程序运行时首先加载实例对象,然后通过实例对象找到VMT,通过VMT找到对应方法的地址,所以虚函数的调用比直接指向方法地址的classic call性能上会差一些。很不幸的是,Java中所有非私有的成员函数的调用都是虚调用。 + +C2编译器已经足够智能,能够检测这种情况并会对虚调用进行优化。比如下面这段代码例子: + +virtual call + +```java +public class SimpleInliningTest +{ + public static void main(String[] args) throws InterruptedException { + VirtualInvokeTest obj = new VirtualInvokeTest(); + VirtualInvoke1 obj1 = new VirtualInvoke1(); + for (int i = 0; i < 100000; i++) { + invokeMethod(obj); + invokeMethod(obj1); + } + Thread.sleep(1000); + } +​ + public static void invokeMethod(VirtualInvokeTest obj) { + obj.methodCall(); + } +​ + private static class VirtualInvokeTest { + public void methodCall() { + System.out.println("virtual call"); + } + } +​ + private static class VirtualInvoke1 extends VirtualInvokeTest { + @Override + public void methodCall() { + super.methodCall(); + } + } +} + +``` + +经过JIT编译器优化后,进行反汇编得到下面这段汇编代码: + +``` + 0x0000000113369d37: callq 0x00000001132950a0 ; OopMap{off=476} + ;*invokevirtual methodCall //代表虚调用 + ; - SimpleInliningTest::invokeMethod@1 (line 18) + ; {optimized virtual_call} //虚调用已经被优化 + +``` + +可以看到JIT对methodCall方法进行了虚调用优化optimized virtual_call。经过优化后的方法可以被内联。但是C2编译器的能力有限,对于多个实现方法的虚调用就“无能为力”了。 + +比如下面这段代码,我们增加一个实现: + +多实现的虚调用 + +```java +public class SimpleInliningTest +{ + public static void main(String[] args) throws InterruptedException { + VirtualInvokeTest obj = new VirtualInvokeTest(); + VirtualInvoke1 obj1 = new VirtualInvoke1(); + VirtualInvoke2 obj2 = new VirtualInvoke2(); + for (int i = 0; i < 100000; i++) { + invokeMethod(obj); + invokeMethod(obj1); + invokeMethod(obj2); + } + Thread.sleep(1000); + } +​ + public static void invokeMethod(VirtualInvokeTest obj) { + obj.methodCall(); + } +​ + private static class VirtualInvokeTest { + public void methodCall() { + System.out.println("virtual call"); + } + } +​ + private static class VirtualInvoke1 extends VirtualInvokeTest { + @Override + public void methodCall() { + super.methodCall(); + } + } + private static class VirtualInvoke2 extends VirtualInvokeTest { + @Override + public void methodCall() { + super.methodCall(); + } + } +} + +``` + +经过反编译得到下面的汇编代码: + +代码块 + +``` + 0x000000011f5f0a37: callq 0x000000011f4fd2e0 ; OopMap{off=28} + ;*invokevirtual methodCall //代表虚调用 + ; - SimpleInliningTest::invokeMethod@1 (line 20) + ; {virtual_call} //虚调用未被优化 + +``` + +可以看到多个实现的虚调用未被优化,依然是virtual_call。 + +Graal编译器针对这种情况,会去收集这部分执行的信息,比如在一段时间,发现前面的接口方法的调用add和sub是各占50%的几率,那么JVM就会在每次运行时,遇到add就把add内联进来,遇到sub的情况再把sub函数内联进来,这样这两个路径的执行效率就会提升。在后续如果遇到其他不常见的情况,JVM就会进行去优化的操作,在那个位置做标记,再遇到这种情况时切换回解释执行。 + +### 3\. 逃逸分析 + +逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”。Java虚拟机的即时编译器会对新建的对象进行逃逸分析,判断对象是否逃逸出线程或者方法。即时编译器判断对象是否逃逸的依据有两种: + +1. 对象是否被存入堆中(静态字段或者堆中对象的实例字段),一旦对象被存入堆中,其他线程便能获得该对象的引用,即时编译器就无法追踪所有使用该对象的代码位置。 +2. 对象是否被传入未知代码中,即时编译器会将未被内联的代码当成未知代码,因为它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中,这种情况,可以直接认为方法调用的调用者以及参数是逃逸的。 + +逃逸分析通常是在方法内联的基础上进行的,即时编译器可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。下面这段代码的就是对象未逃逸的例子: + +```java +pulbic class Example{ + public static void main(String[] args) { + example(); + } + public static void example() { + Foo foo = new Foo(); + Bar bar = new Bar(); + bar.setFoo(foo); + } + } +​ + class Foo {} +​ + class Bar { + private Foo foo; + public void setFoo(Foo foo) { + this.foo = foo; + } + } +} + +``` + +在这个例子中,创建了两个对象foo和bar,其中一个作为另一个方法的参数提供。该方法setFoo()存储对收到的Foo对象的引用。如果Bar对象在堆上,则对Foo的引用将逃逸。但是在这种情况下,编译器可以通过逃逸分析确定Bar对象本身不会对逃逸出example()的调用。这意味着对Foo的引用也不能逃逸。因此,编译器可以安全地在栈上分配两个对象。 + +**锁消除** + +在学习Java并发编程时会了解锁消除,而锁消除就是在逃逸分析的基础上进行的。 + +如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没就有意义。因为线程并不能获得该锁对象。在这种情况下,即时编译器会消除对该不逃逸锁对象的加锁、解锁操作。实际上,编译器仅需证明锁对象不逃逸出线程,便可以进行锁消除。由于Java虚拟机即时编译的限制,上述条件被强化为证明锁对象不逃逸出当前编译的方法。不过,基于逃逸分析的锁消除实际上并不多见。 + +**栈上分配** + +我们都知道Java的对象是在堆上分配的,而堆是对所有对象可见的。同时,JVM需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存。如果逃逸分析能够证明某些新建的对象不逃逸,那么JVM完全可以将其分配至栈上,并且在new语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象。不过Hotspot虚拟机,并没有进行实际的栈上分配,而是使用了标量替换这一技术。所谓的标量,就是仅能存储一个值的变量,比如Java代码中的基本类型。与之相反,聚合量则可能同时存储多个值,其中一个典型的例子便是Java的对象。编译器会在方法内将未逃逸的聚合量分解成多个标量,以此来减少堆上分配。下面是一个标量替换的例子: + +标量替换 + +```java +public class Example{ + @AllArgsConstructor + class Cat{ + int age; + int weight; + } + public static void example(){ + Cat cat = new Cat(1,10); + addAgeAndWeight(cat.age,Cat.weight); + } +} + +``` + +经过逃逸分析,cat对象未逃逸出example()的调用,因此可以对聚合量cat进行分解,得到两个标量age和weight,进行标量替换后的伪代码: + +```java +public class Example{ + @AllArgsConstructor + class Cat{ + int age; + int weight; + } + public static void example(){ + int age = 1; + int weight = 10; + addAgeAndWeight(age,weight); + } +} + +``` + +**部分逃逸分析** + +部分逃逸分析也是Graal对于概率预测的应用。通常来说,如果发现一个对象逃逸出了方法或者线程,JVM就不会去进行优化,但是Graal编译器依然会去分析当前程序的执行路径,它会在逃逸分析基础上收集、判断哪些路径上对象会逃逸,哪些不会。然后根据这些信息,在不会逃逸的路径上进行锁消除、栈上分配这些优化手段。 + +### 4\. Loop Transformations + +在文章中介绍C2编译器的部分有提及到,C2编译器在构建Ideal Graph后会进行很多的全局优化,其中就包括对循环的转换,最重要的两种转换就是循环展开和循环分离。 + +**循环展开** + +循环展开是一种循环转换技术,它试图以牺牲程序二进制码大小为代价来优化程序的执行速度,是一种用空间换时间的优化手段。 + +循环展开通过减少或消除控制程序循环的指令,来减少计算开销,这种开销包括增加指向数组中下一个索引或者指令的指针算数等。如果编译器可以提前计算这些索引,并且构建到机器代码指令中,那么程序运行时就可以不必进行这种计算。也就是说有些循环可以写成一些重复独立的代码。比如下面这个循环: + +循环展开 + +```java +public void loopRolling(){ + for(int i = 0;i<200;i++){ + delete(i); + } +} + +``` + +上面的代码需要循环删除200次,通过循环展开可以得到下面这段代码: + +循环展开 + +```java +public void loopRolling(){ + for(int i = 0;i<200;i+=5){ + delete(i); + delete(i+1); + delete(i+2); + delete(i+3); + delete(i+4); + } +} + +``` + +这样展开就可以减少循环的次数,每次循环内的计算也可以利用CPU的流水线提升效率。当然这只是一个示例,实际进行展开时,JVM会去评估展开带来的收益,再决定是否进行展开。 + +**循环分离** + +循环分离也是循环转换的一种手段。它把循环中一次或多次的特殊迭代分离出来,在循环外执行。举个例子,下面这段代码: + +循环分离 + +```java +int a = 10; +for(int i = 0;i<10;i++){ + b[i] = x[i] + x[a]; + a = i; +} + +``` + +可以看出这段代码除了第一次循环a = 10以外,其他的情况a都等于i-1。所以可以把特殊情况分离出去,变成下面这段代码: + +循环分离 + +```java +b[0] = x[0] + 10; +for(int i = 1;i<10;i++){ + b[i] = x[i] + x[i-1]; +} + +``` + +这种等效的转换消除了在循环中对a变量的需求,从而减少了开销。 + +### 5\. 窥孔优化与寄存器分配 + +前文提到的窥孔优化是优化的最后一步,这之后就会程序就会转换成机器码,窥孔优化就是将编译器所生成的中间代码(或目标代码)中相邻指令,将其中的某些组合替换为效率更高的指令组,常见的比如强度削减、常数合并等,看下面这个例子就是一个强度削减的例子: + +强度削减 + +``` +y1=x1*3 经过强度削减后得到 y1=(x1<<1)+x1 + +``` + +编译器使用移位和加法削减乘法的强度,使用更高效率的指令组。 + +寄存器分配也是一种编译的优化手段,在C2编译器中普遍的使用。它是通过把频繁使用的变量保存在寄存器中,CPU访问寄存器的速度比内存快得多,可以提升程序的运行速度。 + +寄存器分配和窥孔优化是程序优化的最后一步。经过寄存器分配和窥孔优化之后,程序就会被转换成机器码保存在codeCache中。 + +## 四、实践 + +即时编译器情况复杂,同时网络上也很少有实战经验,以下是我们团队的一些调整经验。 + +### 1\. 编译相关的重* 要参数 + +* -XX:+TieredCompilation:开启分层编译,JDK8之后默认开启 +* -XX:+CICompilerCount=N:编译线程数,设置数量后,JVM会自动分配线程数,C1:C2 = 1:2 +* -XX:TierXBackEdgeThreshold:OSR编译的阈值 +* -XX:TierXMinInvocationThreshold:开启分层编译后各层调用的阈值 +* -XX:TierXCompileThreshold:开启分层编译后的编译阈值 +* -XX:ReservedCodeCacheSize:codeCache最大大小 +* -XX:InitialCodeCacheSize:codeCache初始大小 + +-XX:TierXMinInvocationThreshold是开启分层编译的情况下,触发编译的阈值参数,当方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数,或者当方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时,便会触发X层即时编译。分层编译开启下会乘以一个系数,系数根据当前编译的方法和编译线程数确定,降低阈值可以提升编译方法数,一些常用但是不能编译的方法可以编译优化提升性能。 + +由于编译情况复杂,JVM也会动态调整相关的阈值来保证JVM的性能,所以不建议手动调整编译相关的参数。除非一些特定的Case,比如codeCache满了停止了编译,可以适当增加codeCache大小,或者一些非常常用的方法,未被内联到,拖累了性能,可以调整内敛层数或者内联方法的大小来解决。 + +### 2\. 通过JITwatch分析编译日志 + +通过增加-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining -XX:+PrintCodeCache -XX:+PrintCodeCacheOnCompilation -XX:+TraceClassLoading -XX:+LogCompilation -XX:LogFile=LogPath参数可以输出编译、内联、codeCache信息到文件。但是打印的编译日志多且复杂很难直接从其中得到信息,可以使用JITwatch的工具来分析编译日志。JITwatch首页的Open Log选中日志文件,点击Start就可以开始分析日志。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-82ee887c-af7d-48d7-88a0-28960e564d4a.png) + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-6158d832-9a0d-4af0-96ff-bf216a9cd5c6.png) + +如上图所示,区域1中是整个项目Java Class包括引入的第三方依赖;区域2是功能区Timeline以图形的形式展示JIT编译的时间轴,Histo是直方图展示一些信息,TopList里面是编译中产生的一些对象和数据的排序,Cache是空闲codeCache空间,NMethod是Native方法,Threads是JIT编译的线程;区域3是JITwatch对日志分析结果的展示,其中Suggestions中会给出一些代码优化的建议,举个例子,如下图中: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-04b2d9ea-7add-4ee5-bf72-61a6bbaa58cf.png) + +我们可以看到在调用ZipInputStream的read方法时,因为该方法没有被标记为热点方法,同时又“太大了”,导致无法被内联到。使用-XX:CompileCommand中inline指令可以强制方法进行内联,不过还是建议谨慎使用,除非确定某个方法内联会带来不少的性能提升,否则不建议使用,并且过多使用对编译线程和codeCache都会带来不小的压力。 + +区域3中的-Allocs和-Locks逃逸分析后JVM对代码做的优化,包括栈上分配、锁消除等。 + +### 3\. 使用Graal编译器 + +由于JVM会去根据当前的编译方法数和编译线程数对编译阈值进行动态的调整,所以实际服务中对这一部分的调整空间是不大的,JVM做的已经足够多了。 + +为了提升性能,在服务中尝试了最新的Graal编译器。只需要使用-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler就可以启动Graal编译器来代替C2编译器,并且响应C2的编译请求,不过要注意的是,Graal编译器与ZGC不兼容,只能与G1搭配使用。 + +前文有提到过,Graal是一个用Java写的即时编译器,它从Java 9开始便被集成自JDK中,作为实验性质的即时编译器。Graal编译器就是脱身于GraalVM,GraalVM是一个高性能的、支持多种编程语言的执行环境。它既可以在传统的 OpenJDK上运行,也可以通过AOT(Ahead-Of-Time)编译成可执行文件单独运行,甚至可以集成至数据库中运行。 + +前文提到过数次,Graal的优化都基于某种假设(Assumption)。当假设出错的情况下,Java虚拟机会借助去优化(Deoptimization)这项机制,从执行即时编译器生成的机器码切换回解释执行,在必要情况下,它甚至会废弃这份机器码,并在重新收集程序profile之后,再进行编译。 + +这些中激进的手段使得Graal的峰值性能要好于C2,而且在Scale、Ruby这种语言Graal表现更加出色,Twitter目前已经在服务中大量的使用Graal来提升性能,企业版的GraalVM使得Twitter服务性能提升了22%。 + +**使用Graal编译器后性能表现** + +在我们的线上服务中,启用Graal编译后,TP9999从60ms -> 50ms ,下降10ms,下降幅度达16.7%。 + +运行过程中的峰值性能会更高。可以看出对于该服务,Graal编译器带来了一定的性能提升。 + +**Graal编译器的问题** + +Graal编译器的优化方式更加激进,因此在启动时会进行更多的编译,Graal编译器本身也需要被即时编译,所以服务刚启动时性能会比较差。 + +考虑的解决办法:JDK 9开始提供工具jaotc,同时GraalVM的Native Image都是可以通过静态编译,极大地提升服务的启动速度的方式,但是GraalVM会使用自己的垃圾回收,这是一种很原始的基于复制算法的垃圾回收,相比G1、ZGC这些优秀的新型垃圾回收器,它的性能并不好。同时GraalVM对Java的一些特性支持也不够,比如基于配置的支持,比如反射就需要把所有需要反射的类配置一个JSON文件,在大量使用反射的服务,这样的配置会是很大的工作量。我们也在做这方面的调研。 + +## 五、总结 + +本文主要介绍了JIT即时编译的原理以及在美团一些实践的经验,还有最前沿的即时编译器的使用效果。作为一项解释型语言中提升性能的技术,JIT已经比较成熟了,在很多语言中都有使用。对于Java服务,JVM本身已经做了足够多,但是我们还应该不断深入了解JIT的优化原理和最新的编译技术,从而弥补JIT的劣势,提升Java服务的性能,不断追求卓越。 + +----- + +原文链接:https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/src/jvm/meituan-9-gc.md b/docs/jvm/meituan-9-gc.md similarity index 98% rename from docs/src/jvm/meituan-9-gc.md rename to docs/jvm/meituan-9-gc.md index c8423cf8f1..e36301a5e5 100644 --- a/docs/src/jvm/meituan-9-gc.md +++ b/docs/jvm/meituan-9-gc.md @@ -1,16 +1,12 @@ --- -title: Java中9种常见的CMS GC问题分析与解决 -shortTitle: 9种常见的CMS GC问题分析与解决 category: - Java核心 + - JVM tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java中9种常见的CMS GC问题分析与解决 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,cms + - Java --- + +# Java中9种常见的CMS GC问题分析与解决 **1.1 引言** @@ -1547,7 +1543,7 @@ gperftools 是 Google 开发的一款非常实用的工具集,它的原理是 ![](https://upload-images.jianshu.io/upload_images/1179389-7aeb1bd968da52f7?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -除了项目本身的原因,还可能有外部依赖导致的泄漏,如 Netty 和 Spring Boot,详细情况可以学习下这两篇文章:《[疑案追踪:Spring Boot内存泄露排查记](http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=26518700+37&idx=2&sn=847fb15d4413354355c33a46a7bccf55&chksm=bd12a7d88a652ecea5789073973abb9545e76a8972c843968a6efd1fb3a918ef07eed8abb37e&scene=21#wechat_redirect)》、《[Netty堆外内存泄露排查盛宴](http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749037&idx=2&sn=d1d6b0348eea5cd80e2c7a56c8a61fa9&chksm=bd12a3e08a652af684fd8d96e81fc0e0fded69dd847051e6b0f791f3726da0415c9552ee2615&scene=21#wechat_redirect)》。 +除了项目本身的原因,还可能有外部依赖导致的泄漏,如 Netty 和 Spring Boot,详细情况可以学习下这两篇文章:《[疑案追踪:Spring Boot内存泄露排查记](http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651750037&idx=2&sn=847fb15d4413354355c33a46a7bccf55&chksm=bd12a7d88a652ecea5789073973abb9545e76a8972c843968a6efd1fb3a918ef07eed8abb37e&scene=21#wechat_redirect)》、《[Netty堆外内存泄露排查盛宴](http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749037&idx=2&sn=d1d6b0348eea5cd80e2c7a56c8a61fa9&chksm=bd12a3e08a652af684fd8d96e81fc0e0fded69dd847051e6b0f791f3726da0415c9552ee2615&scene=21#wechat_redirect)》。 **4.8.4 小结** @@ -1713,13 +1709,6 @@ JNI 产生的 GC 问题较难排查,需要谨慎使用。 最后的最后,再多啰嗦一句,目前所有 GC 调优相关的文章,第一句讲的就是“不要过早优化”,使得很多同学对 GC 优化望而却步。在这里笔者提出不一样的观点,熵增定律(在一个孤立系统里,如果没有外力做功,其总混乱度(即熵)会不断增大)在计算机系统同样适用,**如果不主动做功使熵减,系统终究会脱离你的掌控**,在我们对业务系统和 GC 原理掌握得足够深的时候,可以放心大胆地做优化,因为我们基本可以预测到每一个操作的结果,放手一搏吧,少年! -原文链接:[https://mp.weixin.qq.com/s/RFwXYdzeRkTG5uaebVoLQw](https://mp.weixin.qq.com/s/RFwXYdzeRkTG5uaebVoLQw) +原文链接:https://mp.weixin.qq.com/s/RFwXYdzeRkTG5uaebVoLQw ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/jvm/neicun-jiegou.md b/docs/jvm/neicun-jiegou.md new file mode 100644 index 0000000000..d030c4e7ea --- /dev/null +++ b/docs/jvm/neicun-jiegou.md @@ -0,0 +1,106 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# 深入理解JVM的内存结构 + + +在谈 JVM 内存区域划分之前,我们先来看一下 Java 程序的具体执行过程,我画了一幅图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/neicun-jiegou-dac0f4c1-8a7e-4309-a599-5664cdaf5016.png) + +Java 源代码文件经过编译器编译后生成字节码文件,然后交给 JVM 的类加载器,加载完毕后,交给执行引擎执行。在整个执行的过程中,JVM 会用一块空间来存储程序执行期间需要用到的数据,这块空间一般被称为运行时数据区,也就是常说的 JVM 内存。 + +所以,当我们在谈 JVM 内存区域划分的时候,其实谈的就是这块空间——运行时数据区。 + +大家应该对官方出品的《Java 虚拟机规范》有所了解吧?了解这个规范可以让我们更深入地理解 JVM。该规范主要包含 6 个部分,分别是: + +- 第一章:引言 +- 第二章:Java 虚拟机结构 +- 第三章:Java 虚拟机编译 +- 第四章:[Class 文件](https://mp.weixin.qq.com/s/uMEZ2Xwctx4n-_8zvtDp5A) +- 第五章:加载、链接和初始化 +- 第六章:Java 虚拟机指令集 +- 第七章:操作码 + +根据第二章 Java 虚拟机结构中的规定,运行时数据区可以分为以下几个部分,见下图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/neicun-jiegou-e33179f3-275b-44c9-87f6-802198f8f360.png) + + +### 01、程序计数器 + +程序计数器(Program Counter Register)所占的内存空间不大,很小一块,可以看作是当前线程所执行的字节码指令的行号指示器。字节码解释器会在工作的时候改变这个计数器的值来选取下一条需要执行的字节码指令,像分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。 + +在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执行时间的,因此,在任一具体时刻,一个 CPU 的内核只会执行一条线程中的指令,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,并且不能互相干扰,否则就会影响到程序的正常执行次序。 + +也就是说,我们要求程序计数器是线程私有的。 + +《Java 虚拟机规范》中规定,如果线程执行的是非本地(native)方法,则程序计数器中保存的是当前需要执行的指令地址;如果线程执行的是本地方法,则程序计数器中的值是 undefined。 + +为什么本地方法在程序计数器中的值是 undefined 的?因为本地方法大多是通过 C/C++ 实现的,并未编译成需要执行的字节码指令。 + +由于程序计数器中存储的数据所占的空间不会随程序的执行而发生大小上的改变,因此,程序计数器是不会发生内存溢出现象(OutOfMemory)的。 + +### 02、Java 虚拟机栈 + +Java 虚拟机栈中是一个个栈帧,每个栈帧对应一个被调用的方法。当线程执行一个方法时,会创建一个对应的栈帧,并将栈帧压入栈中。当方法执行完毕后,将栈帧从栈中移除。[栈](https://mp.weixin.qq.com/s/fc48Z5tSMlBHweYIS1UL0g)遵循的是后进先出的原则,所以线程当前执行的方法对应的栈帧必定在 Java 虚拟机栈的顶部。 + +栈帧包含以下 5 个部分,见下图。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/neicun-jiegou-4ea2a60a-05df-4ed1-8109-99ae23acefd1.png) + +[Java 虚拟机栈](https://tobebetterjavaer.com/jvm/how-jvm-run-zijiema-zhiling.md) + +### 04、堆 + +堆是所有线程共享的一块内存区域,在 Java 虚拟机启动的时候创建,用来存储对象(数组也是一种对象)。 + +以前,Java 中“几乎”所有的对象都会在堆中分配,但随着 JIT(Just-In-Time)编译器的发展和逃逸技术的逐渐成熟,所有的对象都分配到堆上渐渐变得不那么“绝对”了。从 JDK 7 开始,Java 虚拟机已经默认开启逃逸分析了,意味着如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。 + +简单解释一下 JIT 和逃逸分析。 + +常见的编译型语言如 C++,通常会把代码直接编译成 CPU 所能理解的机器码来运行。而 Java 为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由 javac 编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java 可能会干不过 C++ 这类编译型语言。 + +为了优化 Java 的性能 ,JVM 在解释器之外引入了 JIT 编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。 + +逃逸分析(Escape Analysis),简单来讲就是,Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。 + +堆是 Java 垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度来看,由于垃圾收集器基本都采用了分代垃圾收集的算法,所以堆还可以细分为:新生代和老年代。新生代还可以细分为:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。 + +堆这最容易出现的就是 OutOfMemoryError 错误,分为以下几种表现形式: + +- `OutOfMemoryError: GC Overhead Limit Exceeded`:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生该错误。 +- `java.lang.OutOfMemoryError: Java heap space`:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发该错误。和本机的物理内存无关,和我们配置的虚拟机内存大小有关! + +### 05、元空间 + +JDK 8 的时候,原有的方法区(更准确的说应该是永久代)被彻底移除,取而代之的是元空间。 + +我们来说说方法区吧。方法区和堆一样,是线程共享的区域,它用来存储已经被 Java 虚拟机加载的类信息、常量、静态变量,以及便器编译后的代码等。 + +在有些地方,方法区也被称为永久代。但其实不能这么理解。 + +>《Java 虚拟机规范》中只规定了有方法区这么一个概念和它的作用,并没有规定如何去实现它。那么不同的 Java 虚拟机可能就会有不同的实现。永久代是 HotSpot 对方法区的一种实现形式。也就是说,永久代只是 HotSpot 中的一个概念,而方法区则是 Java 虚拟机规范中的一个定义,一种规范。 + +换句话说,方法区和永久代的关系就像是 Java 中接口和类的关系,类实现了接口。 + +在方法区中,还有一块非常重要的部分,也就是运行时常量池。在讲 [class 文件](https://mp.weixin.qq.com/s/uMEZ2Xwctx4n-_8zvtDp5A)的时候,提到了每个 class 文件都会有个常量池,用来存放字符串常量、类和接口的名字、字段名、常量等等。运行时常量池和 class 文件的常量池是一一对应的,它就是通过 class 文件中的常量池来构建的。 + +JDK 7 之前,运行时常量池中包含着字符串常量池,都在方法区。 + +JDK 7 的时候,字符串常量池从方法区中拿出来放到了堆中,运行时常量池中的其他东西还在方法区中。 + +JDK 8 的时候,HotSpot 移除了永久代,也就是说方法区不存在了,取而代之的是元空间。也就意味着字符串常量池在堆中,运行时常量池跑到了元空间。 + +再来说说为什么要将永久代 (PermGen) 或者说方法区替换为元空间 (MetaSpace) 。 + +第一,永久代放在 Java 虚拟机中,就会受到 Java 虚拟机内存大小的限制,而元空间使用的是本地内存,也就脱离了 Java 虚拟机内存的限制。 + +第二,JDK 8 的时候,在 HotSpot 中融合了 JRockit 虚拟机,而 JRockit 中并没有永久代的概念,因此新的 HotSpot 就没有必要再开辟一块空间来作为永久代了。 + + diff --git a/docs/jvm/oom.md b/docs/jvm/oom.md new file mode 100644 index 0000000000..3f89182213 --- /dev/null +++ b/docs/jvm/oom.md @@ -0,0 +1,137 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# 一次内存溢出排查优化实战 + +## 前言 + +`OutOfMemoryError` 问题相信很多朋友都遇到过,相对于常见的业务异常(数组越界、空指针等)来说这类问题是很难定位和解决的。 + +本文以最近碰到的一次线上内存溢出的定位、解决问题的方式展开;希望能对碰到类似问题的同学带来思路和帮助。 + +主要从`表现-->排查-->定位-->解决` 四个步骤来分析和解决问题。 + + + +## 表象 + +最近我们生产上的一个应用不断的爆出内存溢出,并且随着业务量的增长出现的频次越来越高。 + +该程序的业务逻辑非常简单,就是从 Kafka 中将数据消费下来然后批量的做持久化操作。 + +而现象则是随着 Kafka 的消息越多,出现的异常的频次就越快。由于当时还有其他工作所以只能让运维做重启,并且监控好堆内存以及 GC 情况。 + +> 重启大法虽好,可是依然不能根本解决问题。 + +## 排查 + +于是我们想根据运维之前收集到的内存数据、GC 日志尝试判断哪里出现问题。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-81051388-0c35-4de6-a3d9-4f546ef4bfec.jpg) + +结果发现老年代的内存使用就算是发生 GC 也一直居高不下,而且随着时间推移也越来越高。 + +结合 jstat 的日志发现就算是发生了 FGC 老年代也已经回收不了,内存已经到顶。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-e79d4da0-fbb1-4918-a8d8-e29d2d64323b.jpg) + +甚至有几台应用 FGC 达到了上百次,时间也高的可怕。 + +这说明应用的内存使用肯定是有问题的,有许多赖皮对象始终回收不掉。 + +## 定位 + +由于生产上的内存 dump 文件非常大,达到了几十G。也是由于我们的内存设置太大有关。 + +所以导致想使用 MAT 分析需要花费大量时间。 + +因此我们便想是否可以在本地复现,这样就要好定位的多。 + +为了尽快的复现问题,我将本地应用最大堆内存设置为 150M。 + + +然后在消费 Kafka 那里 Mock 为一个 while 循环一直不断的生成数据。 + +同时当应用启动之后利用 VisualVM 连上应用实时监控内存、GC 的使用情况。 + +结果跑了 10 几分钟内存使用并没有什么问题。根据图中可以看出,每产生一次 GC 内存都能有效的回收,所以这样并没有复现问题。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-4cf05af0-924f-406b-a8a4-5aa885e38cea.jpg) + + +没法复现问题就很难定位了。于是我们 review 代码,发现生产的逻辑和我们用 while 循环 Mock 数据还不太一样。 + +查看生产的日志发现每次从 Kafka 中取出的都是几百条数据,而我们 Mock 时每次只能产生**一条**。 + +为了尽可能的模拟生产情况便在服务器上跑着一个生产者程序,一直源源不断的向 Kafka 中发送数据。 + +果然不出意外只跑了一分多钟内存就顶不住了,观察左图发现 GC 的频次非常高,但是内存的回收却是相形见拙。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-a6d6c9cd-e79c-4a76-ba97-032cfefefd5f.jpg) + +同时后台也开始打印内存溢出了,这样便复现出问题。 + +## 解决 + +从目前的表现来看就是内存中有许多对象一直存在强引用关系导致得不到回收。 + +于是便想看看到底是什么对象占用了这么多的内存,利用 VisualVM 的 HeapDump 功能可以立即 dump 出当前应用的内存情况。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-49b47ca3-b3e2-49f7-85c9-23f7a3ef6f93.jpg) + +结果发现 `com.lmax.disruptor.RingBuffer` 类型的对象占用了将近 50% 的内存。 + +看到这个包自然就想到了 `Disruptor` 环形队列。 + +再次 review 代码发现:从 Kafka 里取出的 700 条数据是直接往 Disruptor 里丢的。 + +这里也就能说明为什么第一次模拟数据没复现问题了。 + +模拟的时候是一个对象放进队列里,而生产的情况是 700 条数据放进队列里。这个数据量是 700 倍的差距。 + +而 Disruptor 作为一个环形队列,再对象没有被覆盖之前是一直存在的。 + +我也做了一个实验,证明确实如此。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-dee49da6-905a-4085-b82e-41e136d422e8.jpg) + +我设置队列大小为 8 ,从 0~9 往里面写 10 条数据,当写到 8 的时候就会把之前 0 的位置覆盖掉,后面的以此类推(类似于 HashMap 的取模定位)。 + +所以在生产上假设我们的队列大小是 1024,那么随着系统的运行最终肯定会导致 1024 个位置上装满了对象,而且每个位置是 700 个! + +于是查看了生产上 Disruptor 的 RingBuffer 配置,结果是:`1024*1024`。 + +这个数量级就非常吓人了。 + +为了验证是否是这个问题,我在本地将该值换为 2 ,一个最小值试试。 + +同样的 128M 内存,也是通过 Kafka 一直源源不断的取出数据。通过监控如下: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/oom-5529781f-1f68-47a7-a3d2-04eba9e9d52e.jpg) + +跑了 20 几分钟系统一切正常,每当一次 GC 都能回收大部分内存,最终呈现锯齿状。 + +这样问题就找到了,不过生产上这个值具体设置多少还得根据业务情况测试才能知道,但原有的 1024*1024 是绝对不能再使用了。 + +## 总结 + +虽然到了最后也就改了一行代码(还没改,直接修改配置),但这排查过程我觉得是有意义的。 + +也会让大部分觉得 JVM 这样的黑盒难以下手的同学有一个直观的感受。 + +`同时也得感叹 Disruptor 东西虽好,也不能乱用哦!` + +相关演示代码查看: + +[https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor) + +**你的点赞与转发是最大的支持。** + +原文链接:https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/ + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) diff --git a/docs/jvm/problem-tools.md b/docs/jvm/problem-tools.md new file mode 100644 index 0000000000..f9797329cf --- /dev/null +++ b/docs/jvm/problem-tools.md @@ -0,0 +1,556 @@ +--- +category: + - Java核心 + - JVM +tag: + - Java +--- + +# Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析) + + +## JDK自带的工具 + +在JDK的bin目录下有很多命令行工具: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-547b1b2c-9fb4-4d1d-9c72-013ec210f6a5.jpg) + +  我们可以看到各个工具的大小基本上都稳定在27kb左右,这个不是JDK开发团队刻意为之的,而是因为这些工具大多数是 `jdk\lib\tools.jar` 类库的一层薄包装而已,他们的主要功能代码是在tools类库中实现的。 + +命令行工具的好处是:当应用程序部署到生产环境后,无论是直接接触物理服务器还是远程telnet到服务器上都会受到限制。而借助tools.jar类库里面的接口,我们可以直接在应用程序中实现功能强大的监控分析功能。 + +### 常用命令: + +这里主要介绍如下几个工具: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-01.png) + +1、jps:查看本机java进程信息 + +2、jstack:打印线程的**栈**信息,制作 线程dump文件 + +3、jmap:打印内存映射信息,制作 堆dump文件 + +4、jstat:性能监控工具 + +5、jhat:内存分析工具,用于解析堆dump文件并以适合人阅读的方式展示出来 + +6、jconsole:简易的JVM可视化工具 + +7、jvisualvm:功能更强大的JVM可视化工具 + +8、javap:查看字节码 + +### JAVA Dump: + +JAVA Dump就是虚拟机运行时的快照,将虚拟机运行时的状态和信息保存到文件中,包括: + +线程dump:包含所有线程的运行状态,纯文本格式 + +堆dump:包含所有堆对象的状态,二进制格式 + +## 1、jps + +显示当前所有java进程pid的命令,我们可以通过这个命令来查看到底启动了几个java进程(因为每一个java程序都会独占一个java虚拟机实例),不过jps有个缺点是只能显示当前用户的进程id,要显示其他用户的还只能用linux的ps命令。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-2017daf6-832a-4673-b776-ad3380e47402.png) + +执行jps命令,会列出所有正在运行的java进程,其中jps命令也是一个java程序。前面的数字就是进程的id,这个id的作用非常大,后面会有相关介绍。 + +**jps -help:** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-031be661-e47e-44f0-9e33-34368b187662.png) + +**jps -l** 输出应用程序main.class的完整package名或者应用程序jar文件完整路径名 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-0ccc96dc-8053-4222-9824-b116f02776a4.png) + +**jps -v** 输出传递给JVM的参数 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-059a3285-4a01-4f7a-a6ed-1cc5dcbf3f18.png) + +**jps失效** + +我们在定位问题过程会遇到这样一种情况,用jps查看不到进程id,用ps -ef | grep java却能看到启动的java进程。 + +要解释这种现象,先来了解下jps的实现机制: + +java程序启动后,会在目录/tmp/hsperfdata_{userName}/下生成几个文件,文件名就是java进程的pid,因此jps列出进程id就是把这个目录下的文件名列一下而已,至于系统参数,则是读取文件中的内容。 + +我们来思考下:**如果由于磁盘满了,无法创建这些文件,或者用户对这些文件没有读的权限。又或者因为某种原因这些文件或者目录被清除,出现以上这些情况,就会导致jps命令失效。** + +如果jps命令失效,而我们又要获取pid,还可以使用以下两种方法: + +``` +1、top | grep java +2、ps -ef |grep java +``` + +## 2、jstack + +主要用于生成指定进程当前时刻的线程快照,线程快照是当前java虚拟机每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-e80d0925-2dcf-4204-b46d-47312df2a673.png) + +**3、jmap** + +主要用于打印指定java进程的共享对象内存映射或堆内存细节。 + +**堆Dump是反映堆使用情况的内存镜像,其中主要包括系统信息、虚拟机属性、完整的线程Dump、所有类和对象的状态等。一般在内存不足,GC异常等情况下,我们会去怀疑内存泄漏,这个时候就会去打印堆Dump。** + +jmap的用法摘要: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-96a70bab-5cee-4068-8ccb-1d35124abeea.png) + +**1、`jmap pid`** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-38d5c9da-e433-43d2-b1bc-3f3634e05497.png) + +打印的信息分别为:共享对象的起始地址、映射大小、共享对象路径的全程。 + +**2、`jmap -heap pid`:查看堆使用情况** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-75acf4c8-393d-43d1-b208-04de1f0ba6bd.png) + +**3、`jmap -histo pid`:查看堆中对象数量和大小** + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/problem-tools-5e42fe47-e1e6-4649-acb5-e17bd277a771.png) + +打印的信息分别是:序列号、对象的数量、这些对象的内存占用大小、这些对象所属的类的全限定名 + +如果是内部类,类名的开头会加上*,如果加上live子参数的话,如jmap -histo:live pid,这个命名会触发一次FUll GC,只统计存活对象 + +**4、`jmap -dump:format=b,file=heapdump pid`:将内存使用的详细情况输出到文件** + +然后使用jhat命令查看该文件:jhat -port 4000 文件名 ,在浏览器中访问http:localhost:4000/ + +总结: + +该命令适用的场景是程序内存不足或者GC频繁,这时候很可能是内存泄漏。通过用以上命令查看堆使用情况、大量对象被持续引用等情况。 + +## **4、jstat** + +主要是对java应用程序的资源和性能进行实时的命令行监控,包括了对heap size和垃圾回收状况的监控。 + +`jstat -