提交 64a3dfc1 编写于 作者: 沉默王二's avatar 沉默王二 💬

vuepress java core

上级 f9a59e8b
......@@ -71,111 +71,101 @@
## Java概述
- [什么是 Java?](docs/overview/what-is-java.md)
- [Java 的发展简史](docs/overview/java-history.md)
- [Java 的优势](docs/overview/java-advantage.md)
- [JDK 和 JRE 有什么区别?](docs/overview/jdk-jre.md)
- [手把手教你安装集成开发环境 Intellij IDEA](docs/overview/idea.md)
- [第一个 Java 程序:Hello World](docs/overview/hello-world.md)
- [什么是Java?Java发展简史,Java的优势](docs/overview/what-is-java.md)
- [JDK和JRE有什么区别?](docs/overview/jdk-jre.md)
- [安装集成开发环境Intellij IDEA](docs/overview/idea.md)
- [第一个Java程序:Hello World](docs/overview/hello-world.md)
## Java基础语法
- [基本数据类型](docs/basic-grammar/basic-data-type.md)
- [流程控制](docs/basic-grammar/flow-control.md)
- [运算符](docs/basic-grammar/operator.md)
- [注释](docs/basic-grammar/javadoc.md)
## 面向对象
- [什么是对象?什么是类](docs/oo/object-class.md)
- [变量](docs/oo/var.md)
- [方法](docs/oo/method.md)
- [构造方法](docs/oo/construct.md)
- [代码初始化块](docs/oo/code-init.md)
- [抽象类](docs/oo/abstract.md)
- [接口](docs/oo/interface.md)
- [static 关键字](docs/oo/static.md)
- [this 和 super 关键字](docs/oo/this-super.md)
- [final 关键字](docs/oo/final.md)
- [instanceof 关键字](docs/oo/instanceof.md)
- [不可变对象](docs/basic-extra-meal/immutable.md)
- [可变参数](docs/basic-extra-meal/varables.md)
- [泛型](docs/basic-extra-meal/generic.md)
- [注解](docs/basic-extra-meal/annotation.md)
- [枚举](docs/basic-extra-meal/enum.md)
- [反射](docs/basic-extra-meal/fanshe.md)
## 字符串String
- [String 为什么是不可变的?](docs/string/immutable.md)
- [字符串常量池](docs/string/constant-pool.md)
- [深入浅出 String.intern](docs/string/intern.md)
- [如何比较两个字符串是否相等?](docs/string/equals.md)
- [如何拼接字符串?](docs/string/join.md)
- [如何拆分字符串?](docs/string/split.md)
## 数组
- [什么是数组?](docs/array/array.md)
- [如何打印数组?](docs/array/print.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面向对象编程
- [怎么理解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 中的集合框架该如何分类?](docs/collection/gailan.md)
- [简单介绍下时间复杂度](docs/collection/big-o.md)
- [ArrayList](docs/collection/arraylist.md)
- [LinkedList](docs/collection/linkedlist.md)
- [ArrayList 和 LinkedList 之增删改查的时间复杂度](docs/collection/list-war-1.md)
- [ArrayList 和 LinkedList 的实现方式以及性能对比](docs/collection/list-war-2.md)
- [Iterator与Iterable有什么区别?](docs/collection/iterator-iterable.md)
- [为什么阿里巴巴强制不要在 foreach 里执行删除操作](docs/collection/fail-fast.md)
- [详细讲解 HashMap 的 hash 原理](docs/collection/hash.md)
- [详细讲解 HashMap 的扩容机制](docs/collection/hashmap-resize.md)
- [HashMap 的加载因子为什么是 0.75?](docs/collection/hashmap-loadfactor.md)
- [为什么 HashMap 是线程不安全的?](docs/collection/hashmap-thread-nosafe.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 I/O
- [Java IO学习整理](docs/io/shangtou.md)
- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](docs/io/BIONIOAIO.md)
## 异常处理
- [聊聊异常处理机制](docs/exception/gailan.md)
- [关于 try-catch-finally](docs/exception/try-catch-finally.md)
- [关于 throw 和 throws](docs/exception/throw-throws.md)
- [关于 try-with-resouces](docs/exception/try-with-resouces.md)
- [异常处理机制到底该怎么用?](docs/exception/shijian.md)
- [一文读懂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)
## 常用工具类
- [数组工具类:Arrays](docs/common-tool/arrays.md)
- [集合工具类:Collections](docs/common-tool/collections.md)
- [简化每一行代码工具类:Hutool](docs/common-tool/hutool.md)
- [Guava,拯救垃圾代码,效率提升N倍](docs/common-tool/guava.md)
- [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)
## Java8新特性
## Java新特性
- [入门Java Stream流](https://mp.weixin.qq.com/s/7hNUjjmqKcHDtymsfG_Gtw)
- [Java 8 Optional 最佳指南](https://mp.weixin.qq.com/s/PqK0KNVHyoEtZDtp5odocA)
- [Lambda 表达式入门](https://mp.weixin.qq.com/s/ozr0jYHIc12WSTmmd_vEjw)
- [Java 8 Stream流详细用法](docs/java8/stream.md)
- [Java 8 Optional最佳指南](docs/java8/optional.md)
- [深入浅出Java 8 Lambda表达式](docs/java8/Lambda.md)
## Java重要知识点
- [Java 中常用的 48 个关键字](docs/basic-extra-meal/48-keywords.md)
- [Java 命名的注意事项](docs/basic-extra-meal/java-naming.md)
- [详解 Java 的默认编码方式 Unicode](docs/basic-extra-meal/java-unicode.md)
- [new Integer(18)与Integer.valueOf(18)有什么区别?](docs/basic-extra-meal/int-cache.md)
- [聊聊自动拆箱与自动装箱](docs/basic-extra-meal/box.md)
- [浅拷贝与深拷贝究竟有什么不一样?](docs/basic-extra-meal/deep-copy.md)
- [为什么重写 equals 时必须重写 hashCode 方法?](docs/basic-extra-meal/equals-hashcode.md)
- [方法重载和方法重写有什么区别?](docs/basic-extra-meal/override-overload.md)
- [Java 到底是值传递还是引用传递?](docs/basic-extra-meal/pass-by-value.md)
- [Java 不能实现真正泛型的原因是什么?](docs/basic-extra-meal/true-generic.md)
- [Java 程序在编译期发生了什么?](docs/basic-extra-meal/what-happen-when-javac.md)
- [Comparable和Comparator有什么区别?](docs/basic-extra-meal/comparable-omparator.md)
- [Java IO 流详细划分](docs/io/shangtou.md)
- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](https://mp.weixin.qq.com/s/QQxrr5yP8X9YdFqIwXDoQQ)
- [为什么 Object 类需要一个 hashCode() 方法呢?](https://mp.weixin.qq.com/s/PcbMQ5VGnPXlcgIsK8AW4w)
- [重写的 11 条规则](https://mp.weixin.qq.com/s/tmaK5DSjQhA0IvTrSvKkQQ)
- [空指针的传说](https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg)
- [彻底弄懂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)
## Java并发编程
......@@ -193,6 +183,7 @@
- [JVM 是什么?](docs/jvm/what-is-jvm.md)
- [Java 创建的对象到底放在哪?](docs/jvm/whereis-the-object.md)
- [Java程序在编译期发生了什么?](docs/basic-extra-meal/what-happen-when-javac.md)
- [图解 Java 垃圾回收机制](https://mp.weixin.qq.com/s/RQGImK3-SrvJfs8eYCiv4A)
- [Java 字节码指令](https://mp.weixin.qq.com/s/GKe9F-IZZnw-f-_fRd_sZQ)
- [轻松看懂 Java 字节码](https://mp.weixin.qq.com/s/DRMBsE4iZjJt4xF-AS4w-g)
......
......@@ -11,7 +11,7 @@ export default defineHopeConfig({
"link",
{
rel: "stylesheet",
href: "//at.alicdn.com/t/font_3180624_h5v71pdvgr9.css",
href: "//at.alicdn.com/t/font_3180624_9bx7n6gym99.css",
},
],
],
......
docs/.vuepress/public/favicon.ico

66.1 KB | W: | H:

docs/.vuepress/public/favicon.ico

46.6 KB | W: | H:

docs/.vuepress/public/favicon.ico
docs/.vuepress/public/favicon.ico
docs/.vuepress/public/favicon.ico
docs/.vuepress/public/favicon.ico
  • 2-up
  • Swipe
  • Onion skin
docs/.vuepress/public/logo.png

92.0 KB | W: | H:

docs/.vuepress/public/logo.png

46.6 KB | W: | H:

docs/.vuepress/public/logo.png
docs/.vuepress/public/logo.png
docs/.vuepress/public/logo.png
docs/.vuepress/public/logo.png
  • 2-up
  • Swipe
  • Onion skin
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" class="icon" viewBox="0 0 3280.944 2800"><path fill="#41b883" d="M1645.332 601.004h375.675L1081.82 2238.478 142.636 601.004h718.477l220.708 379.704 216.013-379.704z"/><path fill="#41b883" d="M142.636 601.004l939.185 1637.474 939.186-1637.474h-375.675l-563.51 982.484-568.208-982.484z"/><path fill="#35495e" d="M513.188 601.004l568.207 987.23 563.511-987.23h-347.498l-216.013 379.704-220.708-379.704zM1607.792 1311.83l594.678 2.293 187.353-316.325-598.662 2.292zM2198.506 1909.57C2867.436 732.7 2939.502 605.426 2937.874 603.78c-.715-.723 45.303-1.314 102.262-1.314s103.562.428 103.562.951c0 .523-208.57 367.978-463.491 816.567L2216.715 2235.6l-102.1.596-102.102.596z"/><path fill="#41b883" d="M1680.563 2233.328c0-1.34 168.208-298.145 440.375-777.048a4135645.775 4135645.775 0 00337.619-594.19l146.13-257.25 170.746-.04 170.747-.04-5.536 9.741c-3.044 5.358-43.727 77.302-90.407 159.875-85.356 150.992-337.562 595.163-656.602 1156.373l-172 302.559-170.536.588c-93.795.322-170.536.069-170.536-.567z"/><path fill="#35495e" d="M1429.783 1625.351l594.679 2.292 187.353-316.324-598.662 2.292z"/><path fill="#41b883" d="M1524.207 1464.903l608.285 6.877 173.746-320.909h-619.072z"/></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="718.000000pt" height="722.000000pt" viewBox="0 0 718.000000 722.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,722.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3390 5675 l0 -275 -22 -4 c-13 -3 -50 -10 -83 -17 -33 -6 -97 -24
-143 -39 l-82 -28 -39 66 c-21 37 -87 150 -146 252 -59 102 -110 189 -115 193
-5 6 -357 -187 -377 -207 -2 -2 64 -121 147 -264 82 -144 150 -263 150 -266 0
-3 -38 -42 -85 -87 -47 -45 -102 -103 -122 -130 -20 -27 -40 -49 -44 -49 -4 0
-108 71 -231 158 -123 87 -228 161 -235 165 -11 7 -50 -43 -196 -253 l-66 -96
250 -174 c137 -96 249 -177 249 -181 0 -3 -13 -35 -29 -70 -51 -115 -103 -261
-137 -393 -18 -72 -32 -131 -31 -132 2 -2 322 -58 393 -69 l41 -6 7 41 c17
105 125 395 194 520 195 354 438 555 757 627 87 19 325 22 410 4 145 -30 296
-96 399 -173 84 -63 201 -185 266 -276 137 -193 223 -391 316 -730 3 -9 14
-11 36 -8 143 24 394 69 396 71 2 2 -13 64 -32 139 -35 134 -90 287 -141 400
-16 34 -24 61 -18 65 5 3 118 82 251 175 l241 169 -18 26 c-170 243 -215 308
-225 318 -9 10 -62 -23 -249 -153 l-238 -167 -16 24 c-9 13 -66 74 -126 136
l-109 111 142 249 c79 136 147 254 153 261 6 8 6 16 1 21 -29 27 -367 212
-374 205 -5 -5 -57 -92 -116 -194 -58 -102 -124 -215 -145 -251 l-39 -67 -62
23 c-35 13 -109 33 -165 46 l-103 23 0 273 0 273 -220 0 -220 0 0 -275z"/>
<path d="M3520 4730 c-223 -35 -417 -215 -469 -434 -17 -70 -15 -204 4 -278
46 -179 195 -338 372 -400 57 -19 87 -23 188 -22 102 0 130 4 185 25 179 68
317 220 365 399 19 72 19 207 -1 284 -50 196 -205 357 -394 410 -72 20 -181
27 -250 16z m160 -447 c89 -62 89 -172 0 -231 -86 -58 -210 13 -210 120 0 34
31 86 65 108 38 25 111 26 145 3z"/>
<path d="M1982 2948 c3 -31 6 -33 51 -40 65 -9 94 -30 107 -78 17 -64 8 -1414
-9 -1462 -48 -129 -159 -155 -219 -51 -15 25 -41 61 -59 80 -81 85 -194 -9
-138 -116 40 -78 136 -116 280 -109 176 7 269 84 322 265 15 51 18 139 22 728
5 518 9 675 19 692 15 27 62 53 96 53 42 0 56 10 56 41 l0 29 -266 0 -265 0 3
-32z"/>
<path d="M2900 2571 c-140 -16 -217 -61 -241 -139 -11 -37 -10 -48 4 -77 13
-26 24 -35 52 -41 58 -11 82 7 119 89 18 40 41 79 50 86 23 19 106 26 149 12
46 -15 93 -69 107 -121 6 -22 10 -68 8 -101 l-3 -60 -157 -68 c-188 -82 -255
-121 -313 -182 -103 -110 -80 -267 50 -337 48 -26 197 -23 257 5 61 28 112 69
145 116 32 45 43 47 43 5 0 -50 36 -114 74 -132 68 -32 183 -12 238 41 30 29
33 40 18 63 -7 12 -12 12 -28 -4 -14 -12 -33 -16 -63 -14 -40 3 -46 7 -64 43
-18 36 -20 68 -25 350 -5 295 -6 312 -27 352 -40 75 -111 108 -250 116 -43 3
-107 2 -143 -2z m248 -543 c-5 -129 -22 -180 -83 -247 -100 -110 -275 -70
-275 63 0 91 66 162 223 241 61 30 117 55 125 55 12 0 13 -20 10 -112z"/>
<path d="M4898 2569 c-90 -9 -164 -39 -203 -80 -22 -22 -29 -41 -33 -82 -4
-49 -2 -56 23 -75 15 -12 38 -22 51 -22 49 0 72 21 105 97 36 84 61 103 131
103 67 0 128 -36 156 -90 19 -37 22 -58 20 -123 l-3 -78 -160 -71 c-257 -113
-334 -173 -361 -280 -25 -97 12 -184 100 -231 57 -30 199 -30 266 0 52 24 129
89 149 127 18 34 31 33 31 -1 1 -36 31 -103 59 -128 35 -30 134 -33 200 -5 64
28 95 63 79 90 -10 16 -14 17 -34 3 -36 -23 -93 -13 -118 20 -20 27 -21 44
-26 340 -5 267 -8 318 -23 352 -37 82 -92 118 -211 134 -85 12 -86 12 -198 0z
m252 -527 c0 -111 -15 -175 -56 -233 -85 -123 -258 -122 -295 1 -32 105 76
223 281 307 30 12 58 23 63 23 4 0 7 -44 7 -98z"/>
<path d="M3490 2531 c0 -25 5 -30 43 -39 45 -12 75 -38 100 -87 8 -16 63 -149
122 -295 59 -146 129 -317 154 -380 l48 -115 54 -3 54 -2 44 102 c65 150 255
594 282 657 28 66 59 105 99 121 24 10 30 19 30 41 l0 29 -175 0 -175 0 0 -29
c0 -26 4 -29 42 -35 58 -9 68 -21 68 -76 0 -64 -21 -125 -128 -379 -89 -209
-99 -229 -105 -220 -4 5 -150 405 -184 502 -25 76 -30 139 -10 155 6 5 31 13
55 17 38 6 42 9 42 36 l0 29 -230 0 -230 0 0 -29z"/>
</g>
</svg>
import { defineSidebarConfig } from "vuepress-theme-hope";
export default defineSidebarConfig([
"",
{
text: "Java核心",
icon: "java",
prefix: "overview/",
collapsable: true,
children: [
{
prefix: "overview/",
text: "Java概述",
icon: "note",
icon: "gaishu",
collapsable: true,
children: ["what-is-java", "java-history", "java-advantage", "jdk-jre"],
children: ["what-is-java", "hello-world"],
},
{
text: "Java基础语法",
icon: "note",
children: ["what-is-java", "java-history", "java-advantage", "jdk-jre"],
icon: "jichuyufa",
collapsable: true,
children: [
"basic-grammar/basic-data-type",
"basic-grammar/flow-control",
"basic-grammar/operator",
"basic-grammar/javadoc",
"basic-extra-meal/48-keywords",
"basic-extra-meal/java-naming"
],
},
{
text: "Java面向对象编程",
icon: "duixiangmoxing",
collapsable: true,
children: [
"oo/object-class",
"oo/var",
"oo/method",
"oo/construct",
"oo/code-init",
"oo/abstract",
"oo/interface",
"oo/static",
"oo/this-super",
"oo/final",
"oo/instanceof",
"basic-extra-meal/immutable",
"basic-extra-meal/varables",
"basic-extra-meal/generic",
"basic-extra-meal/annotation",
"basic-extra-meal/enum",
"basic-extra-meal/fanshe"
],
},
{
text: "字符串&数组",
icon: "Field-String",
collapsable: true,
children: [
"string/immutable",
"string/constant-pool",
"string/intern",
"string/equals",
"string/join",
"string/split",
"array/array",
"array/print"
],
},
{
text: "集合框架(容器)",
icon: "rongqi",
collapsable: true,
children: [
"collection/gailan",
"collection/arraylist",
"collection/linkedlist",
"collection/list-war-2",
"collection/iterator-iterable",
"collection/fail-fast",
"collection/hashmap",
],
},
{
text: "Java IO",
icon: "shurushuchu",
collapsable: true,
children: [
"io/shangtou",
"io/BIONIOAIO",
],
},
{
text: "异常处理",
icon: "yichangchuli",
collapsable: true,
children: [
"exception/gailan",
"exception/try-with-resouces",
"exception/shijian",
"exception/npe"
],
},
{
text: "常用工具类",
icon: "gongju",
collapsable: true,
children: [
"common-tool/arrays",
"common-tool/collections",
"common-tool/hutool",
"common-tool/guava"
],
},
{
text: "Java新特性",
icon: "xintexing",
collapsable: true,
children: [
"java8/stream",
"java8/optional",
"java8/Lambda",
],
},
{
text: "Java重要知识点",
icon: "zhongyaotishi",
collapsable: true,
children: [
"basic-extra-meal/java-unicode",
"basic-extra-meal/int-cache",
"basic-extra-meal/box",
"basic-extra-meal/deep-copy",
"basic-extra-meal/hashcode",
"basic-extra-meal/equals-hashcode",
"basic-extra-meal/override-overload",
"basic-extra-meal/Overriding",
"basic-extra-meal/pass-by-value",
"basic-extra-meal/true-generic",
"basic-extra-meal/comparable-omparator",
],
},
],
},
......
---
category:
- Java核心
tag:
- Java
---
# 深入理解Java数组
“哥,我看你之前的文章里提到,ArrayList 的内部是用数组实现的,我就对数组非常感兴趣,想深入地了解一下,今天终于到这个环节了,好期待呀!”三妹的语气里显得很兴奋。
“的确是的,看 ArrayList 的源码就一清二楚了。”我一边说,一边打开 Intellij IDEA,并找到了 ArrayList 的源码。
......
---
category:
- Java核心
tag:
- Java
---
# 如何优雅地打印Java数组?
“哥,之前听你说,数组也是一个对象,但 Java 中并未明确的定义这样一个类。”看来三妹有在用心地学习。
“是的,因此数组也就没有机会覆盖 `Object.toString()` 方法。如果尝试直接打印数组的话,输出的结果并不是我们预期的结果。”我接着三妹的话继续说。
......@@ -149,8 +158,4 @@ System.out.println(Arrays.deepToString(deepArray));
“OK,我走,我走。”
-----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
---
category:
- Java核心
tag:
- Java
---
# Java中常用的48个关键字
“二哥,就我之前学过的这些 Java 代码中,有 public、static、void、main 等等,它们应该都是关键字吧?”三妹的脸上泛着甜甜的笑容,我想她在学习 Java 方面已经变得越来越自信了。
“是的,三妹。Java 中的关键字可不少呢!你一下子可能记不了那么多,不过,先保留个印象吧,对以后的学习会很有帮助。”
......
---
category:
- Java核心
tag:
- Java
---
# Java重写(Overriding)时应当遵守的11条规则
重写(Overriding)算是 Java 中一个非常重要的概念,理解重写到底是什么对每个 Java 程序员来说都至关重要,这篇文章就来给大家说说重写过程中应当遵守的 12 条规则。
### 01、什么是重写?
重写带来了一种非常重要的能力,可以让子类重新实现从超类那继承过来的方法。在下面这幅图中,Animal 是父类,Dog 是子类,Dog 重新实现了 `move()` 方法用来和父类进行区分,毕竟狗狗跑起来还是比较有特色的。
![](https://cdn.jsdelivr.net/gh/itwanger/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 的,所以子类在尝试重写该方法的时候就出现编译错误了!
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/basic-extra-meal/Overriding-2.png)
同样的,如果一个方法是 static 的,也不允许重写,因为静态方法可用于父类以及子类的所有实例。
```java
public class Animal {
final void move() { }
}
```
重写的目的在于根据对象的类型不同而表现出多态,而静态方法不需要创建对象就可以使用。没有了对象,重写所需要的“对象的类型”也就没有存在的意义了。
![](https://cdn.jsdelivr.net/gh/itwanger/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;
}
}
```
于是就编译出错了(返回类型不兼容)。
![](https://cdn.jsdelivr.net/gh/itwanger/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() { }
}
```
如果子类中的方法用了更严格的权限修饰符,编译器就报错了。
![](https://cdn.jsdelivr.net/gh/itwanger/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 关键和重写规则无关。
---
category:
- Java核心
tag:
- Java
---
# 深入理解Java注解
“二哥,这节讲注解吗?”三妹问。
“是的。”我说,“注解是 Java 中非常重要的一部分,但经常被忽视也是真的。之所以这么说是因为我们更倾向成为一名注解的使用者而不是创建者。`@Override` 注解用过吧?但你知道怎么自定义一个注解吗?”
......@@ -214,10 +223,3 @@ public class JsonFieldTest {
“撸个注解好像真没什么难度,但你接下来的那个 JsonSerializer 我还需要再消化一下。”三妹很认真地说。
“嗯,你好好复习下,我看会《编译原理》。”说完我拿起桌子边上的一本书就走了。
-----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongzhonghao.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# 深入剖析Java中的拆箱和装箱
“哥,听说 Java 的每个基本类型都对应了一个包装类型,比如说 int 的包装类型为 Integer,double 的包装类型为 Double,是这样吗?”从三妹这句话当中,能听得出来,她已经提前预习这块内容了。
......
---
category:
- Java核心
tag:
- Java
---
# 详解Java中Comparable和Comparator的区别
那天,小二去马蜂窝面试,面试官老王一上来就甩给了他一道面试题:请问Comparable和Comparator有什么区别?小二差点笑出声,因为三年前,也就是 2021 年,他在《Java 程序员进阶之路》专栏上看到过这题😆。
*PS:星标这种事,只能求,不求没效果,come on。《Java 程序员进阶之路》在 GitHub 上已经收获了 565 枚星标,小伙伴们赶紧去点点了,冲 600*
>https://github.com/itwanger/toBeBetterJavaer
Comparable 和 Comparator 是 Java 的两个接口,从名字上我们就能够读出来它们俩的相似性:以某种方式来比较两个对象。但它们之间到底有什么区别呢?请随我来,打怪进阶喽!
### 01、Comparable
......
---
category:
- Java核心
tag:
- Java
---
# 彻底讲明白的Java浅拷贝与深拷贝
“哥,听说浅拷贝和深拷贝是 Java 面试中经常会被问到的一个问题,是这样吗?”
......
---
category:
- Java核心
tag:
- Java
---
# Java枚举(enum)
“今天我们来学习枚举吧,三妹!”我说,“同学让你去她家玩了两天,感觉怎么样呀?”
......@@ -281,9 +288,3 @@ public enum EasySingleton{
“好勒,这就安排。二哥,你去休息吧。”
“嗯嗯。”讲了这么多,必须跑去抽烟机那里安排一根华子了。
-----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# 一次性搞清楚equals和hashCode
“二哥,我在读《Effective Java》 的时候,第 11 条规约说重写 equals 的时候必须要重写 hashCode 方法,这是为什么呀?”三妹单刀直入地问。
......@@ -223,9 +230,3 @@ result = (31*1 + Integer(18).hashCode()) * 31 + String("张三").hashCode();
“OK,get 了。”三妹开心地点了点头,看得出来,今天学到了不少。
-----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
---
category:
- Java核心
tag:
- Java
---
# 大白话说Java反射:入门、使用、原理
“二哥,什么是反射呀?”三妹开门见山地问。
......@@ -313,9 +320,4 @@ Method[] methods2 = System.class.getMethods();
>链接:https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html
-----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
---
category:
- Java核心
tag:
- Java
---
# 深入理解Java泛型
“二哥,为什么要设计泛型啊?”三妹开门见山地问。
......@@ -305,11 +312,14 @@ public class Cmower {
但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”):
```
>Erasure of method method(Arraylist<String>) is the same as another method in type
Cmower
>
>Erasure of method method(Arraylist<Date>) is the same as another method in type
Cmower
```
大致的意思就是,这两个方法的参数类型在擦除后是相同的。
......
---
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 的取值来决定采用哪一种哈希值的生成策略。
![](https://cdn.jsdelivr.net/gh/itwanger/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<Student, Integer> 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 为字符串长度)。
![](https://cdn.jsdelivr.net/gh/itwanger/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<intptr_t>(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<intptr_t>(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 算法,简单来说,看起来就是一个移位寄存器,每次移入的位由寄存器中若干位取异或生成。所谓的伪随机数,不是完全随机的,但是真随机生成比较困难,所以只要能通过一定的随机数统计检测,就可以当作真随机数来使用。
---
category:
- Java核心
tag:
- Java
---
# 深入理解Java中的不可变对象
>二哥,你能给我说说为什么 String 是 immutable 类(不可变对象)吗?我想研究它,想知道为什么它就不可变了,这种强烈的愿望就像想研究浩瀚的星空一样。但无奈自身功力有限,始终觉得雾里看花终隔一层。二哥你的文章总是充满趣味性,我想一定能够说明白,我也一定能够看明白,能在接下来写一写吗?
收到读者小 R 的私信后,我就总感觉自己有一种义不容辞的责任,非要把 immutable 类说明白不可!
*PS:star 这种事,只能求,不求没效果,铁子们,《Java 程序员进阶之路》在 GitHub 上已经收获了 523 枚星标,铁子们赶紧去点点了,冲 600 star*
>https://github.com/itwanger/toBeBetterJavaer
### 01、什么是不可变类
......
---
category:
- Java核心
tag:
- Java
---
# Java中int、Integer、new Integer之间的区别
“三妹,今天我们来补一个小的知识点:Java 数据类型缓存池。”我喝了一口枸杞泡的茶后对三妹说,“考你一个问题哈:`new Integer(18) 与 Integer.valueOf(18) ` 的区别是什么?”
......
---
category:
- Java核心
tag:
- Java
---
# Java命名规范(非常全面,可以收藏)
“二哥,Java 中的命名约定都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。
......
---
category:
- Java核心
tag:
- Java
---
# 彻底弄懂Java中的Unicode和UTF-8编码
“二哥,[上一篇](https://mp.weixin.qq.com/s/twim3w_dp5ctCigjLGIbFw)文章中提到了 Unicode,说 Java 中的
char 类型之所以占 2 个字节,是因为 Java 使用的是 Unicode 字符集而不是 ASCII 字符集,我有点迷,想了解一下,能细致给我说说吗?”
......
---
category:
- Java核心
tag:
- Java
---
# Java重写(Override)与重载(Overload)
### 01、开篇
......
---
category:
- Java核心
tag:
- Java
---
# Java到底是值传递还是引用传递?
“哥,说说 Java 到底是值传递还是引用传递吧?”三妹一脸的困惑,看得出来她被这个问题折磨得不轻。
......
---
category:
- Java核心
tag:
- Java
---
# Java不能实现真正泛型的原因是什么?
“二哥,为啥 Java 不能实现真正泛型啊?”三妹开门见山地问。
......@@ -22,7 +29,9 @@ public class Cmower {
但由于类型擦除的原因,以上代码是不会编译通过的——编译器会提示一个错误:
```
>'method(ArrayList<String>)' clashes with 'method(ArrayList<Date>)'; both methods have same erasure
```
也就是说,两个 `method()` 方法经过类型擦除后的方法签名是完全相同的,Java 是不允许这样做的。
......
---
category:
- Java核心
tag:
- Java
---
# Java中可变参数的使用
为了让铁粉们能白票到阿里云的服务器,老王当了整整两天的客服,真正体验到了什么叫做“为人民群众谋福利”的不易和辛酸。正在他眼睛红肿打算要休息之际,小二跑过来问他:“Java 的可变参数究竟是怎么一回事?”老王一下子又清醒了,他爱 Java,他爱传道解惑,他爱这群尊敬他的读者。
*PS:star 这种事,只能求,不求没效果,铁子们,《Java 程序员进阶之路》在 GitHub 上已经收获了 514 枚星标,铁子们赶紧去点点了,冲 600 star*
>https://github.com/itwanger/toBeBetterJavaer
可变参数是 Java 1.5 的时候引入的功能,它允许方法使用任意多个、类型相同(`is-a`)的值作为参数。就像下面这样。
```java
......
---
category:
- Java核心
tag:
- Java
---
# Java程序在编译期发生了什么?
“二哥,看了上一篇 [Hello World](https://mp.weixin.qq.com/s/191I_2CVOxVuyfLVtb4jhg) 的程序后,我很好奇,它是怎么在 Run 面板里打印出‘三妹,少看手机少打游戏,好好学,美美哒’呢?”三妹咪了一口麦香可可奶茶后对我说。
......
---
category:
- Java核心
tag:
- Java
---
# Java 支持的 8 种基本数据类型
“二哥,[上一节](https://mp.weixin.qq.com/s/IgBpLGn0L1HZymgI4hWGVA)提到了 Java 变量的数据类型,是不是指定了类型就限定了变量的取值范围啊?”三妹吸了一口麦香可可奶茶后对我说。
......@@ -296,13 +303,13 @@ public class ArrayList<E> extends AbstractList<E>
基本数据类型:
1、变量名指向具体的数值。
2、基本数据类型存储在栈上。
- 1、变量名指向具体的数值。
- 2、基本数据类型存储在栈上。
引用数据类型:
1、变量名指向的是存储对象的内存地址,在栈上。
2、内存地址指向的对象存储在堆上。
- 1、变量名指向的是存储对象的内存地址,在栈上。
- 2、内存地址指向的对象存储在堆上。
看到这,三妹是不是又要问,“堆是什么,栈又是什么?”
......
---
category:
- Java核心
tag:
- Java
---
# Java流程控制语句
“二哥,流程控制语句都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。
......@@ -899,8 +906,3 @@ public class ContinueDoWhileDemo {
注意:同样的,如果把 if 条件中的“i++”省略掉的话,程序就会进入死循环,一直在 continue。
-----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# Java注释:单行、多行和文档注释
“二哥,Java 中的注释好像真没什么可讲的,我已经提前预习了,不过是单行注释,多行注释,还有文档注释。”三妹的脸上泛着甜甜的笑容,她竟然提前预习了接下来要学习的知识,有一种“士别三日,当刮目相看”的感觉。
......
---
category:
- Java核心
tag:
- Java
---
# Java运算符
“二哥,让我盲猜一下哈,运算符是不是指的就是加减乘除啊?”三妹的脸上泛着甜甜的笑容,我想她一定对提出的问题很有自信。
......
---
category:
- Java核心
tag:
- Java
---
# Java集合ArrayList详解
“二哥,听说今天我们开讲 ArrayList 了?好期待哦!”三妹明知故问,这个托配合得依然天衣无缝。
......
“二哥,为什么要讲时间复杂度呀?”三妹问。
“因为接下来要用到啊。后面我们学习 ArrayList、LinkedList 的时候,会比较两者在增删改查时的执行效率,而时间复杂度是衡量执行效率的一个重要标准。”我说。
“到时候跑一下代码,统计一下前后的时间差不更准确吗?”三妹反问道。
“实际上,你说的是另外一种评估方法,这种评估方法可以得出非常准确的数值,但也有很大的局限性。”我不急不慢地说。
第一,测试结果会受到测试环境的影响。你比如说,同样的代码,在我这台 iMac 上跑出来的时间和在你那台华为的 MacBook 上抛出的时间可能就差别很大。
第二,测试结果会受到测试数据的影响。你比如说,一个排序后的数组和一个没有排序后的数组,调用了同一个查询方法,得出来的结果可能会差别特别大。
“因此,我们需要这种不依赖于具体测试环境和测试数据就能粗略地估算出执行效率的方法,时间复杂度就是其中的一种,还有一种是空间复杂度。”我继续补充道。
来看下面这段代码:
```java
public static int sum(int n) {
int sum = 0; // 第 1 行
for (int i=0;i<n;i++) { // 第 2 行
sum = sum + 1; // 第 3 行
} // 第 4 行
return sum; // 第 5 行
}
```
这段代码非常简单,方法体里总共 5 行代码,包括“}”那一行。每段代码的执行时间可能都不大一样,但假设我们认为每行代码的执行时间是一样的,比如说 unit_time,那么这段代码总的执行时间为多少呢?
“这个我知道呀!”三妹喊道,“第 1、5 行需要 2 个 unit_time,第 2、3 行需要 2*n*unit_time,总的时间就是 2(n+1)*unit_time。”
“对,一段代码的执行时间 T(n) 和总的执行次数成正比,也就是说,代码执行的次数越多,花费的时间就越多。”我总结道,“这个规律可以用一个公式来表达:”
> 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 的时候,两者在增删改查时的执行效率就很容易对比清楚了。”我抬起头看了看三妹说,她似乎有些明白,又有些不太明白。
“不要担心哥,我再温习一遍就能搞懂了。”三妹很乖。
----------
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# 为什么阿里巴巴强制不要在foreach里执行删除操作
那天,小二去阿里面试,面试官老王一上来就甩给了他一道面试题:为什么阿里的 Java 开发手册里会强制不要在 foreach 里进行元素的删除操作?小二听完就面露喜色,因为两年前,也就是 2021 年,他在《Java 程序员进阶之路》专栏上的第 63 篇看到过这题😆。
*PS:star 这种事,只能求,不求没效果,铁子们,《Java 程序员进阶之路》在 GitHub 上已经收获了 417 枚星标,小伙伴们赶紧去点点了,冲 500 star!*
>https://github.com/itwanger/toBeBetterJavaer
那天,小二去阿里面试,面试官老王一上来就甩给了他一道面试题:为什么阿里的 Java 开发手册里会强制不要在 foreach 里进行元素的删除操作?
-----
......
---
category:
- Java核心
tag:
- Java
---
# Java集合框架
眼瞅着三妹的王者荣耀杀得正嗨,我趁机喊到:“别打了,三妹,我们来一起学习 Java 的集合框架吧。”
......@@ -190,7 +198,97 @@ HashMap 是无序的,所以遍历的时候元素的顺序也是不可测的。
为了保证顺序,TreeMap 的键必须要实现 Comparable 接口或者 Comparator 接口。
“好了,三妹,整体上,集合框架就这么多东西了,随后我们会一一展开来讲,比如说 ArrayList、LinkedList、HashMap 等。”我伸了个懒腰后对三妹说。
### 05、时间复杂度
“二哥,为什么要讲时间复杂度呀?”三妹问。
“因为接下来要用到啊。后面我们学习 ArrayList、LinkedList 的时候,会比较两者在增删改查时的执行效率,而时间复杂度是衡量执行效率的一个重要标准。”我说。
“到时候跑一下代码,统计一下前后的时间差不更准确吗?”三妹反问道。
“实际上,你说的是另外一种评估方法,这种评估方法可以得出非常准确的数值,但也有很大的局限性。”我不急不慢地说。
第一,测试结果会受到测试环境的影响。你比如说,同样的代码,在我这台 iMac 上跑出来的时间和在你那台华为的 MacBook 上抛出的时间可能就差别很大。
第二,测试结果会受到测试数据的影响。你比如说,一个排序后的数组和一个没有排序后的数组,调用了同一个查询方法,得出来的结果可能会差别特别大。
“因此,我们需要这种不依赖于具体测试环境和测试数据就能粗略地估算出执行效率的方法,时间复杂度就是其中的一种,还有一种是空间复杂度。”我继续补充道。
来看下面这段代码:
```java
public static int sum(int n) {
int sum = 0; // 第 1 行
for (int i=0;i<n;i++) { // 第 2 行
sum = sum + 1; // 第 3 行
} // 第 4 行
return sum; // 第 5 行
}
```
这段代码非常简单,方法体里总共 5 行代码,包括“}”那一行。每段代码的执行时间可能都不大一样,但假设我们认为每行代码的执行时间是一样的,比如说 unit_time,那么这段代码总的执行时间为多少呢?
“这个我知道呀!”三妹喊道,“第 1、5 行需要 2 个 unit_time,第 2、3 行需要 2*n*unit_time,总的时间就是 2(n+1)*unit_time。”
“对,一段代码的执行时间 T(n) 和总的执行次数成正比,也就是说,代码执行的次数越多,花费的时间就越多。”我总结道,“这个规律可以用一个公式来表达:”
> 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 等。”。
“好的,二哥。”三妹重新回答沙发上,一盘王者荣耀即将开始。
那天,小二去蔚来面试,面试官老王一上来就问他: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<K,V>[] tab; HashMap.Node<K,V> 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<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> 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<K,V> 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 方法呢?
看下面这个图。
![](https://cdn.jsdelivr.net/gh/itwanger/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 方法就是为了增加随机性,让数据元素更加均衡的分布,减少碰撞**
参考链接:
> https://blog.csdn.net/lonyw/article/details/80519652
>https://zhuanlan.zhihu.com/p/91636401
>https://www.zhihu.com/question/20733617
\ No newline at end of file
---
category:
- 求职面试
tag:
- Java
---
# Java HashMap精选面试题
对于 Java 求职者来说,HashMap 可谓是重中之重,是面试的必考点。然而 HashMap 的知识点非常多,复习起来花费精力很大。
......
**Warning**:这是《Java 程序员进阶之路》专栏的第 57 篇,我们来聊聊 HashMap的加载因子,为什么必须是0.75,而不是0.8,0.6。
本文 GitHub 上已同步,有 GitHub 账号的小伙伴,记得给二哥安排一波 star 呀!冲 GitHub 的 trending 榜单,求求各位了。
>GitHub 地址:https://github.com/itwanger/toBeBetterJavaer
>在线阅读地址:https://itwanger.gitee.io/tobebetterjavaer
-------
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
具体是用这么一个公式来表示的。
![](https://cdn.jsdelivr.net/gh/itwanger/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的概率,由上面公式得:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-02.png)
这个概率值需要大于0.5,我们认为这样的hashmap可以提供很低的碰撞率。所以:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-03png)
这时候,我们对于该公式其实最想求的时候长度s的时候,n为多少次就应该进行扩容了?而负载因子则是$n/s$的值。所以推导如下:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-04.png)
所以可以得到
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-05.png)
其中
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-06.png)
这就是一个求 `∞⋅0`函数极限问题,这里我们先令$s = m+1(m \to \infty)$则转化为
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-07.png)
我们再令 $x = \frac{1}{m} (x \to 0)$ 则有,
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-08.png)
所以,
![](https://cdn.jsdelivr.net/gh/itwanger/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 是个比较完美的选择。
**HashMap 发出的 Warning**:这是《Java 程序员进阶之路》专栏的第 56 篇。那天,小二垂头丧气地跑来给我诉苦,“老王,有个学弟小默问我‘ HashMap 的扩容机制’,我愣是支支吾吾讲了半天,没给他讲明白,讲到最后我内心都是崩溃的,差点哭出声!”
我安慰了小二好一会,他激动的情绪才稳定下来。我给他说,HashMap 的扩容机制本来就很难理解,尤其是 JDK8 新增了红黑树之后。先基于 JDK7 讲,再把红黑树那块加上去就会容易理解很多。
小二这才恍然大悟,佩服地点了点头。
**HashMap 发出的呼声**:有 GitHub 账号的小伙伴记得去安排一波 star 呀,《Java 程序员进阶之路》开源教程目前在 GitHub 上有 244 个 star 了,准备冲 1000 了,求求各位了。
>GitHub 地址:https://github.com/itwanger/toBeBetterJavaer
>在线阅读地址:https://itwanger.gitee.io/tobebetterjavaer
-------
大家都知道,数组一旦初始化后大小就无法改变了,所以就有了 [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<K,V> e : table) {
while(null != e) {
// 拉链法,相同 key 上的不同值
Entry<K,V> 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。那么扩容前的样子如下图所示。
![](https://cdn.jsdelivr.net/gh/itwanger/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] 上。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-resize-02.png)
按照我们的预期,扩容后的 7 仍然应该在 3 这条链表的后面,但实际上呢? 7 跑到 3 这条链表的头部了。针对 JDK 7 中的这个情况,JDK 8 做了哪些优化呢?
看下面这张图。
![](https://cdn.jsdelivr.net/gh/itwanger/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,也就是原来的索引+原来的容量
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-resize-04.png)
也就是说,JDK 8 不需要像 JDK 7 那样重新计算 hash,只需要看原来的hash值新增的那个bit是1还是0就好了,是0的话就表示索引没变,是1的话,索引就变成了“原索引+原来的容量”。
![](https://cdn.jsdelivr.net/gh/itwanger/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<K,V>[] resize() {
Node<K,V>[] 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 小数组复制到大数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 链表优化重 hash 的代码块
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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;
}
```
>参考链接:https://zhuanlan.zhihu.com/p/21673805
三方面原因:多线程下扩容会死循环、多线程下 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<K,V> e : table) {
while(null != e) {
// 拉链法,相同 key 上的不同值
Entry<K,V> 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` 这两行代码,就会将同一位置上的新元素被放在链表的头部。
扩容前的样子假如是下面这样子。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-01.png)
那么正常扩容后就是下面这样子。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-02.png)
假设现在有两个线程同时进行扩容,线程 A 在执行到 `newTable[i] = e;` 被挂起,此时线程 A 中:e=3、next=7、e.next=null
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-03.png)
线程 B 开始执行,并且完成了数据转移。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-04.png)
此时,7 的 next 为 3,3 的 next 为 null。
随后线程A获得CPU时间片继续执行 `newTable[i] = e`,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:
![](https://cdn.jsdelivr.net/gh/itwanger/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 了。
采用头部插入的方式,变成了下面这样子:
![](https://cdn.jsdelivr.net/gh/itwanger/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 被头插法重新插入到链表中,执行结果如下图所示:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-07.png)
套娃开始,元素 5 也就成了弃婴,惨~~~
不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序,参照[HashMap 扩容机制](https://mp.weixin.qq.com/s/0KSpdBJMfXSVH63XadVdmw)的这一篇。
### 02、多线程下 put 会导致元素丢失
正常情况下,当发生哈希冲突时,HashMap 是这样的:
![](https://cdn.jsdelivr.net/gh/itwanger/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<K,V>[] tab; Node<K,V> 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<K,V> 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<K,V>)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 是这样的:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-09.png)
接着,线程 B 执行了 ` tab[i] = newNode(hash, key, value, null)`,那 table 是这样的:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-10.png)
3 被干掉了。
### 03、put 和 get 并发时会导致 get 到 null
线程 A 执行put时,因为元素个数超出阈值而出现扩容,线程B 此时执行get,有可能导致这个问题。
注意来看 resize 源码:
```java
final Node<K,V>[] resize() {
Node<K,V>[] 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
}
```
线程 A 执行完 `table = newTab` 之后,线程 B 中的 table 此时也发生了变化,此时去 get 的时候当然会 get 到 null 了,因为元素还没有转移。
\ No newline at end of file
---
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<K,V>[] tab; HashMap.Node<K,V> 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<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> 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<K,V> 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 方法呢?
看下面这个图。
![](https://cdn.jsdelivr.net/gh/itwanger/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<K,V> e : table) {
while(null != e) {
// 拉链法,相同 key 上的不同值
Entry<K,V> 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。那么扩容前的样子如下图所示。
![](https://cdn.jsdelivr.net/gh/itwanger/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] 上。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-resize-02.png)
按照我们的预期,扩容后的 7 仍然应该在 3 这条链表的后面,但实际上呢? 7 跑到 3 这条链表的头部了。针对 JDK 7 中的这个情况,JDK 8 做了哪些优化呢?
看下面这张图。
![](https://cdn.jsdelivr.net/gh/itwanger/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,也就是原来的索引+原来的容量
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-resize-04.png)
也就是说,JDK 8 不需要像 JDK 7 那样重新计算 hash,只需要看原来的hash值新增的那个bit是1还是0就好了,是0的话就表示索引没变,是1的话,索引就变成了“原索引+原来的容量”。
![](https://cdn.jsdelivr.net/gh/itwanger/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<K,V>[] resize() {
Node<K,V>[] 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 小数组复制到大数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 链表优化重 hash 的代码块
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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
具体是用这么一个公式来表示的。
![](https://cdn.jsdelivr.net/gh/itwanger/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的概率,由上面公式得:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-02.png)
这个概率值需要大于0.5,我们认为这样的hashmap可以提供很低的碰撞率。所以:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-03png)
这时候,我们对于该公式其实最想求的时候长度s的时候,n为多少次就应该进行扩容了?而负载因子则是$n/s$的值。所以推导如下:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-04.png)
所以可以得到
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-05.png)
其中
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-06.png)
这就是一个求 `∞⋅0`函数极限问题,这里我们先令$s = m+1(m \to \infty)$则转化为
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-07.png)
我们再令 $x = \frac{1}{m} (x \to 0)$ 则有,
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-08.png)
所以,
![](https://cdn.jsdelivr.net/gh/itwanger/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<K,V> e : table) {
while(null != e) {
// 拉链法,相同 key 上的不同值
Entry<K,V> 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` 这两行代码,就会将同一位置上的新元素被放在链表的头部。
扩容前的样子假如是下面这样子。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-01.png)
那么正常扩容后就是下面这样子。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-02.png)
假设现在有两个线程同时进行扩容,线程 A 在执行到 `newTable[i] = e;` 被挂起,此时线程 A 中:e=3、next=7、e.next=null
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-03.png)
线程 B 开始执行,并且完成了数据转移。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-04.png)
此时,7 的 next 为 3,3 的 next 为 null。
随后线程A获得CPU时间片继续执行 `newTable[i] = e`,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:
![](https://cdn.jsdelivr.net/gh/itwanger/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 了。
采用头部插入的方式,变成了下面这样子:
![](https://cdn.jsdelivr.net/gh/itwanger/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 被头插法重新插入到链表中,执行结果如下图所示:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-07.png)
套娃开始,元素 5 也就成了弃婴,惨~~~
不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序,参照[HashMap 扩容机制](https://mp.weixin.qq.com/s/0KSpdBJMfXSVH63XadVdmw)的这一篇。
### 02、多线程下 put 会导致元素丢失
正常情况下,当发生哈希冲突时,HashMap 是这样的:
![](https://cdn.jsdelivr.net/gh/itwanger/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<K,V>[] tab; Node<K,V> 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<K,V> 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<K,V>)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 是这样的:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-09.png)
接着,线程 B 执行了 ` tab[i] = newNode(hash, key, value, null)`,那 table 是这样的:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-10.png)
3 被干掉了。
### 03、put 和 get 并发时会导致 get 到 null
线程 A 执行put时,因为元素个数超出阈值而出现扩容,线程B 此时执行get,有可能导致这个问题。
注意来看 resize 源码:
```java
final Node<K,V>[] resize() {
Node<K,V>[] 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<K,V>[] newTab = (Node<K,V>[])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
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# Java中的Iterator和Iterable区别
那天,小二去海康威视面试,面试官老王一上来就甩给了他一道面试题:请问 Iterator与Iterable有什么区别?小二差点笑出声,因为一年前,也就是 2021 年,他在《Java 程序员进阶之路》专栏上的第 62 篇看到过这题😆。
......
---
category:
- Java核心
tag:
- Java
---
# Java集合LinkedList详解
### 一、LinkedList 的剖白
......
这是《Java 程序员进阶之路》专栏的第 60 篇,我们来聊聊 ArrayList 和 LinkedList 之间的区别。大家可以到 GitHub 上给二哥一个 star,马上破 400 星标了。
>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer)
如果再有人给你说 “**ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快**”,你可以让他滚了!
这是一个极其不负责任的总结,关键是你会在很多地方看到这样的结论。
害,我一开始学 Java 的时候,也问过一个大佬,“ArrayList 和 LinkedList 有什么区别?”他就把“ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”甩给我了,当时觉得,大佬好牛逼啊!
后来我研究了 ArrayList 和 LinkedList 的源码,发现还真的是,前者是数组,后者是 LinkedList,于是我对大佬更加佩服了!
直到后来,我亲自跑程序验证了一遍,才发现大佬的结论太草率了!根本就不是这么回事!
先来给大家普及一个概念——[时间复杂度](https://mp.weixin.qq.com/s/e7SbkEPPx1OExsAG4qV6Gw)
>在计算机科学中,算法的时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大 O 符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。例如,如果一个算法对于任何大小为 n (必须比 $n_0$ 大)的输入,它至多需要 $5n^3 + 3n$ 的时间运行完毕,那么它的渐近时间复杂度是 $O(n3^)$。
增删改查,对应到 ArrayList 和 LinkedList,就是 add(E e)、remove(int index)、add(int index, E element)、get(int index),我来给大家一一分析下,它们对应的时间复杂度,也就明白了“ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”这个结论很荒唐的原因
**对于 ArrayList 来说**
1)`get(int index)` 方法的时间复杂度为 $O(1)$,因为是直接从底层数组根据下标获取的,和数组长度无关。
```java
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
```
这也是 ArrayList 的最大优点。
2)`add(E e)` 方法会默认将元素添加到数组末尾,但需要考虑到数组扩容的情况,如果不需要扩容,时间复杂度为 $O(1)$。
```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;
}
```
如果需要扩容的话,并且不是第一次(`oldCapacity > 0`)扩容的时候,内部执行的 `Arrays.copyOf()` 方法是耗时的关键,需要把原有数组中的元素复制到扩容后的新数组当中。
```java
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)];
}
}
```
3)`add(int index, E element)` 方法将新的元素插入到指定的位置,考虑到需要复制底层数组(根据之前的判断,扩容的话,数组可能要复制一次),根据最坏的打算(不管需要不需要扩容,`System.arraycopy()` 肯定要执行),所以时间复杂度为 $O(n)$。
```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 的位置上。
```java
ArrayList<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("沉默王四");
list.add("沉默王五");
list.add("沉默王六");
list.add("沉默王七");
list.add(2, "沉默王八");
```
`System.arraycopy()` 执行完成后,下标为 2 的元素为沉默王四,这一点需要注意。也就是说,在数组中插入元素的时候,会把插入位置以后的元素依次往后复制,所以下标为 2 和下标为 3 的元素都为沉默王四。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/list-war-1-01.png)
之后再通过 `elementData[index] = element` 将下标为 2 的元素赋值为沉默王八;随后执行 `size = s + 1`,数组的长度变为 7。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/list-war-1-02.png)
4)` remove(int index)` 方法将指定位置上的元素删除,考虑到需要复制底层数组,所以时间复杂度为 $O(n)$。
```java
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;
}
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;
}
```
**对于 LinkedList 来说**
1)`get(int index)` 方法的时间复杂度为 $O(n)$,因为需要循环遍历整个链表。
```java
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
LinkedList.Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
LinkedList.Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
LinkedList.Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
```
下标小于链表长度的一半时,从前往后遍历;否则从后往前遍历,这样从理论上说,就节省了一半的时间。
如果下标为 0 或者 `list.size() - 1` 的话,时间复杂度为 $O(1)$。这种情况下,可以使用 `getFirst()``getLast()` 方法。
```java
public E getFirst() {
final LinkedList.Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final LinkedList.Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
```
first 和 last 在链表中是直接存储的,所以时间复杂度为 $O(1)$。
2)`add(E e)` 方法默认将元素添加到链表末尾,所以时间复杂度为 $O(1)$。
```java
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final LinkedList.Node<E> l = last;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
```
3)`add(int index, E element)` 方法将新的元素插入到指定的位置,需要先通过遍历查找这个元素,然后再进行插入,所以时间复杂度为 $O(n)$。
```java
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
```
如果下标为 0 或者 `list.size() - 1` 的话,时间复杂度为 $O(1)$。这种情况下,可以使用 `addFirst()``addLast()` 方法。
```java
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final LinkedList.Node<E> f = first;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
```
`linkFirst()` 只需要对 first 进行更新即可。
```java
public void addLast(E e) {
linkLast(e);
}
void linkLast(E e) {
final LinkedList.Node<E> l = last;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
```
`linkLast()` 只需要对 last 进行更新即可。
需要注意的是,有些文章里面说,LinkedList 插入元素的时间复杂度近似 $O(1)$,其实是有问题的,因为 `add(int index, E element)` 方法在插入元素的时候会调用 `node(index)` 查找元素,该方法之前我们之间已经确认过了,时间复杂度为 $O(n)$,即便随后调用 `linkBefore()` 方法进行插入的时间复杂度为 $O(1)$,总体上的时间复杂度仍然为 $O(n)$ 才对。
```java
void linkBefore(E e, LinkedList.Node<E> succ) {
// assert succ != null;
final LinkedList.Node<E> pred = succ.prev;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
```
4)` remove(int index)` 方法将指定位置上的元素删除,考虑到需要调用 `node(index)` 方法查找元素,所以时间复杂度为 $O(n)$。
```java
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
E unlink(LinkedList.Node<E> x) {
// assert x != null;
final E element = x.item;
final LinkedList.Node<E> next = x.next;
final LinkedList.Node<E> 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;
}
```
通过时间复杂度的比较,以及源码的分析,我相信大家在选择的时候就有了主意,对吧?
需要注意的是,如果列表很大很大,ArrayList 和 LinkedList 在**内存**的使用上也有所不同。LinkedList 的每个元素都有更多开销,因为要存储上一个和下一个元素的地址。ArrayList 没有这样的开销。
查询的时候,ArrayList 比 LinkedList 快,这是毋庸置疑的;插入和删除的时候,LinkedList 因为要遍历列表,所以并不比 ArrayList 更快。反而 ArrayList 更轻量级,不需要在每个元素上维护上一个和下一个元素的地址。
但是,请注意,如果 ArrayList 在增删改的时候涉及到大量的数组复制,效率就另当别论了,因为这个过程相当的耗时。
对于初学者来说,一般不会涉及到百万级别的数据操作,如果真的不知道该用 ArrayList 还是 LinkedList,就无脑选择 ArrayList 吧!
------
这是《Java 程序员进阶之路》专栏的第 60 篇。Java 程序员进阶之路,风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点。
>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer)
这么好的东西,还不 star 下?
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# Java中ArrayList和LinkedList的区别
这是《Java 程序员进阶之路》专栏的第 61 篇,我们来继续探讨 ArrayList 和 LinkedList,这一篇比[上一篇](https://mp.weixin.qq.com/s/mjeLeNv5PKateVarZE4KQQ)更深入、更全面,源码讲解、性能考量,方方面面都有涉及到了。
首先必须得感谢大家,《Java 程序员进阶之路》在 GitHub 上已经突破 400 个星标了,感谢感谢,还没 star 的赶紧安排一波了,冲击 500 星标了。
>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer)
### 01、ArrayList 是如何实现的?
......@@ -838,9 +841,3 @@ private class ListItr implements ListIterator<E> {
### 06、总结
花了两天时间,终于肝完了!相信看完这篇文章后,再有面试官问你 ArrayList 和 LinkedList 有什么区别的话,你一定会胸有成竹地和他扯上半小时了。
这是《Java 程序员进阶之路》专栏的第 61 篇。Java 程序员进阶之路,风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点。
>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer)
这么硬核的东西,还不赶紧 star 下?
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# Java Arrays工具类10大常用方法
“哥,数组专用工具类是专门用来操作数组的吗?比如说创建数组、数组排序、数组检索等等。”三妹的提问其实已经把答案说了出来。
......
---
category:
- Java核心
tag:
- Java
---
# Java集合框架:Collections工具类
Collections 是 JDK 提供的一个工具类,位于 java.util 包下,提供了一系列的静态方法,方便我们对集合进行各种骚操作,算是集合框架的一个大管家。
......
---
category:
- Java核心
tag:
- Java
---
# Google开源的Guava工具库,太强大了~
### 01、前世今生
你好呀,我是 Guava。
......
---
category:
- Java核心
tag:
- Java
---
# Hutool:国产良心工具包,让你的Java变得更甜
读者群里有个小伙伴感慨说,“Hutool 这款开源类库太厉害了,基本上该有该的工具类,它里面都有。”讲真的,我平常工作中也经常用 Hutool,它确实可以帮助我们简化每一行代码,使 Java 拥有函数式语言般的优雅,让 Java 语言变得“甜甜的”。
PS:为了能够帮助更多的 Java 爱好者,已将《Java 程序员进阶之路》开源到了 GitHub(本篇已收录)。该专栏目前已经收获了 598 枚星标,如果你也喜欢这个专栏,**觉得有帮助的话,可以去点个 star,这样也方便以后进行更系统化的学习**
>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer)
Hutool 的作者在官网上说,Hutool 是 Hu+tool 的自造词(好像不用说,我们也能猜得到),“Hu”用来致敬他的“前任”公司,“tool”就是工具的意思,谐音就有意思了,“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”(一个开源类库,上升到了哲学的高度,作者厉害了)。
看了一下开发团队的一个成员介绍,一个 Java 后端工具的作者竟然爱前端、爱数码,爱美女,嗯嗯嗯,确实“难得糊涂”(手动狗头)。
......
---
category:
- Java核心
tag:
- Java
---
# 一文读懂Java异常处理
## 一、什么是异常
“二哥,今天就要学习异常了吗?”三妹问。
......@@ -46,6 +55,8 @@ Exception in thread "main" java.lang.ArithmeticException: / by zero
“你看,三妹,这个原生的异常信息对用户来说,显然是不太容易理解的,但对于我们开发者来说,简直不要太直白了——很容易就能定位到异常发生的根源。”
## 二、Exception和Error的区别
“哦,我知道了。下一个问题,我经常看到一些文章里提到 Exception 和 Error,二哥你能帮我解释一下它们之间的区别吗?”三妹问。
“这是一个好问题呀,三妹!”
......@@ -60,6 +71,8 @@ Exception 的出现,意味着程序出现了一些在可控范围内的问题
比如说之前提到的 ArithmeticException,很明显是因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作,当然了,更好的做法是直接对除数进行判断,如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。
## 三、checked和unchecked异常
“三妹,还能想到其他的问题吗?”
“嗯,不用想,二哥,我已经提前做好预习工作了。”三妹自信地说,“异常又可以分为 checked 和 unchecked,它们之间又有什么区别呢?”
......@@ -164,11 +177,287 @@ public class Demo2 {
或者说,强制性的 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 中。
![](https://cdn.jsdelivr.net/gh/itwanger/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` 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。
好的,二哥,你去休息吧。
三妹,来看一下源码的文档注释就全明白了!
“对了,三妹,我定个姑婆婆的外卖吧,晚上我们喝粥。”
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/exception/try-catch-finally-01.png)
“好呀,我要两个豆沙包。”
至于参数 status 的值也很好理解,如果是异常退出,设置为非 0 即可,通常用 1 来表示;如果是想正常退出程序,用 0 表示即可。
---
category:
- Java核心
tag:
- Java
---
# Java空指针NullPointerException的传说
**空指针**,号称天下最强刺客。
他原本不叫这个名字,空指针原本复姓**异常**,空指针只不过是他的武器,但他杀戮过多,渐渐地人们只记住了空指针这三个字。
天下武功,唯快不破,空指针的针,以快和诡异著称,稍有不慎,便是伤亡。
... ...
我叫铁柱,我来到这个奇怪的世界已经一年了,我等了一年,穿越附赠的老爷爷、戒指、系统什么的我到现在都没发现。
而且这个世界看起来也太奇怪了,这里好像叫什么 **Java** 大陆,我只知道这个世界的最强者叫做 **Object**,听说是什么道祖级的存在,我也不知道是什么意思,毕竟我现在好像还是个菜鸡,别的主角一年都应该要飞升仙界了吧,我还连个小火球都放不出来。
哦,对了,上面的那段话是我在茶馆喝茶的时候听说书的先生说的,总觉得空指针这个名字怪怪的,好像在什么地方听说过。
我的头痛的毛病又犯了,我已经记不起来我为什么来到这里了,我只记得我的名字叫铁柱,其他的,我只感觉这个奇怪的世界有一种熟悉,但是我什么都记不起来了。
算了,得过且过吧。
我准备去找空指针了,虽然听说他很可怕,但是好像听说他不是嗜杀之人,应该不会滥杀无辜吧,目前为止,我也只对这三个字有熟悉的感觉了,我一定要找到他,找回我的记忆!
我打听了很久,原来空指针是**异常组织**的三代嫡传,异常组织是这个世界上最恐怖的杀手组织,空指针就是异常现在最出色的刺客。
听说空指针出生的时候,脖子上就挂着一根针,整个 Java 大陆雪下一月不停,Linux 森林多块陆地直接沉陷,于是他的父亲 **RuntimeException** 就给他起了空指针这个名字。
空指针出生的天生异象也引起了异常组织高层的注意,听说他的祖父 **Exception**,还有整个异常组织的领军人物 **Throwable** 都亲自接见了空指针,并且认为空指针天赋异禀,未来可期。
要知道,**Throwable** 可是 **Object** 亲自任命的异常组织头领。作为 Object 最值得信任的亲信,跟随 Object 万年以来,所有的脏活累活都依靠 Thrwoable 创立的异常组织来处理,真可谓一人之下,万人之上。
Throwable 只有两个亲子,就是 Error 和 Exception,传说中 Error 心狠手辣,手下无一活口,见过 Error 的人还能活下来的寥寥无几。
整个大陆只有他们恐怖的传说,谁也不知道他们什么时候出现,但是一旦他们出现,基本宣告着你已经是个死人了。
而我听说过最恐怖的就是`OutOfMemoryError` 和 `StackOverflowError` 这两位刺客,因为大陆上永远有一座风云榜悬挂在帝都门口,而这两位,一直位居杀手榜榜首位置,空指针也只只能屈居第三而已。当然,大陆不少人都认为空指针会后来居上。
我的消息只是打听到这么多,接下来的日子,我走过无数的城市、荒野,我穿过沙漠、丛林,这一天,终于,我来到了大陆的帝都--**堆**
这个名字听起来也有点耳熟,不管他,先进城再说。
进城后我发现这里非常诡异,整座城市好像都非常年轻,好像连一个成年人都没有!街道上熙熙攘攘竟然都是年轻人。
带着疑惑,我走进了一家叫做*同福客栈*的酒楼。
”客官,打尖还是住店啊?“一个小二模样的*小孩*带着一丝谄媚的对我说。
”住店,带我去最好的房间,这些钱先押你这里,不够再跟我要。“一路走来,对于这些地方的行情我也算轻车熟路了。
”小朋友,这里是怎么回事?你们这里没有大人吗?“我一边走一边问这个只有我一半身高的小孩,根据我目测,他身高不超过1米,应该还只有七八岁的样子,难道这里的商人如此黑心,竟然雇佣童工,不过这也不貌似不对,因为周围的客人好像也都是这般年纪,他们竟然还有在抽烟喝酒的!
”客官可真幽默,不过我看客官应该是刚来帝都,不瞒您说,整个帝都就基本没有超过15岁的人,超过15的据说都在叫做老年区的养老去了!就拿我来说吧,我今年可不小了,我都8岁了,像我这般年纪的已经半截腿迈进棺材咯。哎,这身子也是一年不如一年了。“
看着这个小二一脸认真的样子,我越发觉得这座城市诡异起来了!8岁,什么鬼?8岁不是应该在家里看喜羊羊吗?!还半截腿迈进棺材!
”可是你看我比你高这么多,你不觉得奇怪吗?“我奇怪的问他。
”有什么好奇怪的,要不是我小时候喝多了三鹿,没准我也长这么高了!“小二有点生气的对我说。
行吧,再说两句把他激怒了,跳起来打我膝盖就大事不妙了。
接下来的几天,经过我的打探,原来我在的地方是叫做年轻区,整个帝都就只有这两个区域,年轻区的人年龄确实没有超过15岁的,有些人刚出生没几天就死了,对此,生活在这里的人也见怪不怪了。对于他们来说,寄希望能活到超过15岁进入老年区养老就是他们的梦想。
我在怀疑是不是异常组织在这里暗杀,可是发现结果并不是,这里的人貌似已经习惯了,生活对他们来说就是随便活活就好了,每次的死亡对于他们来说毫无征兆,可能刚踢着球呢,就突然挂了,有的上着厕所突然就死了,临死前连个屁股都没擦,不说了,有点恶心。
就在我等的不耐烦想打算去老年区看看的时候,一个穿着黑衣的人找到了我。
”你是谁?“我警惕的问他。
”本座IOException。“黑衣人神情冰冷的看着我说。
”你找我什么事?“
”这些你不用知道,跟我走一趟吧!“
我刚想说话拒绝,开什么玩笑,跟你们异常组打交道的人非死即残,谁要跟你去。
但是由不得我拒绝,我只感觉一阵天旋地转,我感觉我在天上飞,然后我就失去了意识。当我醒来的时候,我发现我躺在一张巨大的床上,桌子上点着一支檀香,整个房间只有一张桌子、一把椅子和我躺的地方。
房间很小,应该只有10几个平方,但是我竟然又有一种熟悉的感觉,这种感觉萦绕在我心头挥散不去。
没等我再想更多,房门打开了。
”是你,你把我带来干什么?“
”走吧,有人要见你。“
还是不容我抗拒,如果我的战斗力是5的话,我想,IO他该有好几万了吧。
又是这该死的眩晕感,不过这次没有几秒钟,我就发现我在一个花园里,花园中间一个身穿黄袍的中年人正在慢悠悠的喝茶。在他身上我感受不到任何强大的气息,甚至不如IOException给我的压迫感强烈。这是谁?
不等我思绪飘飞,IOException弯腰躬身说道:”陛下,人带过来了。“
”嗯,你退下吧。“中年人转过身来,脸上丝毫看不出情绪的说道。
我大概猜到了这是哪里了,于是也放下心来,在这里,或许能找到我的答案。
反正他要对我怎么样,我也没有办法反抗,我径直坐到他的对面,看着他说:”您就是Object陛下吧,不知找我所谓何事?“
中年人也不在意,没有正面回答我的问题,反而略带一丝调侃的说道:”不用咬文嚼字,说点正常人的话吧。“
... ...
这不按套路出牌啊,我这不是来久了,模仿你们古代人说话嘛,怎么还埋怨起我来了?!
”那我就直说了,我想知道空指针在哪里。“
”空指针就在皇宫轮值,你找他干嘛?“
”我暂时不能说“
”呵呵,你就不好奇我为什么知道你,为什么又把你带过来?“
”好奇,可是我就是不想问。“
Object喝了口茶,不紧不慢的回道:”年轻人有性格是好事,可是过刚易折的道理你应该明白。“
”我不明白,我在这里反正也没看见什么老人,当然,除了你。“我理所当然的认为这肯定是Object搞得鬼,整个帝都都是小朋友,要是没有猫腻,骗鬼呢!
Object听到这话,皱了皱眉,他沉默了一会儿,缓缓站起身子走到一颗柳树下,背着手说道:“你不知道这一切是为什么吗?”
废话,我当然不知道了,我知道还能问你吗?!
又是沉默... ...这个气氛让我感觉很不舒服。就在我受不了想说话的时候,Object突然说了一句:“带他去见空指针吧。”
“是,陛下!”突然,一个身穿红袍的枯瘦老者出现在我背后,把我吓了一跳。
我也不想再多生事端,直觉告诉我这里不是久留之地,虽然有点莫名其妙,我还是跟着红袍老者来走了。
... ...
“陛下,是他吗?”一个光头大汉的身影在半空若影若现的说道。
“还不能确定... 不过,留给我们的时间不多了,下一次的轮回就快来了。”
“轮回,又是轮回。我们还有希望吗?”大汉呢喃着,不知道是对自己说还是对中年人说。
中年人依然背着手,抬头望着漫天的柳絮说道:“这一世,该是个了断了。”
... ...
没多久,他把我带到一个房间门口,也是面无表情的说道:“进去吧,空指针就在里面。”
我挺住脚步,转过身问他:“你是谁?我们是不是见过?”
红袍老者怪异一笑:“也许吧,老夫`IndexOutOfBoundsException`,空指针便是我好友。”
这个名字可真长,我听说过他,据传闻他的实力也非常之强,可能不下于空指针,都是以诡异的出手角度著称,不过相比于空指针的大名,他好像更低调,难怪在皇宫当个老太监一般。
我也不在多想,点点头,走进了房间。刚进房间,我就看见一个一身白衣的身影背对着我,笔直的身影好像要冲破天际,身上的气势强大无比,至少在我见过的所有人里足以排进前三了。空指针,果然名不虚传!
我走到房间中央,环目四望才发现这好像是一座祠堂的样子,就在我还在打量四周之际,一道清冷的声音传到我的耳边:
“你身上的气息让我非常讨厌!”
他转过身来,我发现我根本看不清空指针长什么样子,他的脸好像打上了马赛克。听到他的话,我心里的疑惑更多了,我只是觉得他的气息让我感到非常熟悉,他的话让我有点莫名其妙。于是我试探道:
“你知道我是谁?”
听到我的话,他一步步走进我,在我身边闻了闻,这让我什么一紧,虽然我想搞清楚我身上的问题,但是我不是出卖肉体的人,我退后一步说:
“你想干嘛?”
空指针皱紧了眉头,仿佛自言自语道:“不对,不对,这是... 规则的气息?可是他明明身上没有任何能量波动。”
我见他好像魔怔了,仿佛在思考什么,于是迈步走到他刚才站立的地方看着前面,原来,这是他们的族谱!这里是异常的祠堂!
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/exception/npe-1)
看完这张族谱,我恍然大悟,好像明白了什么。突然,我的脑袋里出现了一个冰冷的机器声音:“获取异常族谱,历练完成度+100。”
我Kao,系统,这是系统啊,我不禁内牛满面,啥任务系统啊,一点提示都没有,我赶紧喊道:
“系统,系统,还在吗?在线等,挺着急的。”可是没有任何回复!这啥破系统!就在我想破口大骂的时候,空指针看到我和个二傻子似的大呼小叫,突然一脸不可思议的对着我说:
“你明悟了规则?”
我愣了愣,嗯?难道我不是战5渣了?规则之力?好像是很高端的样子啊?
“撒豆成兵!”
“呼风!”
”唤雨!“
”临兵斗者皆阵列在前!“
一点反应都没有。。。啥玩意儿?还规则之力?九字真言都没用啊?
空指针好像都蒙了,他敲了敲太阳穴,无语的看着我说:
”你不是来找我的吗?说完你的问题,然后给我滚!“
对啊,这系统把我整的我都忘记我来干嘛的了,我赶紧说:
”你认识我对不对,你是不是觉得我有一种熟悉的感觉?我想知道我的来历!“
空指针又愣了愣,他看着我,沉默了一会儿,回道:“不知道!”
我有点奇怪,看他一脸便秘的表情应该是见过我的,他一定在撒谎,既然如此...
“那你告诉我你们有什么办法能在你们异常的攻击下防身吧?”
空指针大怒,刚想起身说话,空中突然传来一道声音:答应他的要求!
他冷哼一声,丢给我一本书,上面写着**catch**一个字,还有一块写着**catch**的令牌,冰冷的说到:“你想知道的都在这里了。”说完,拂袖而去。
我看着桌子上的这本书,想了想还是翻阅起来。
原来`Exception` 和它的儿子们,除了`RuntimeException` 一支,都叫作`Checked Exception`,我还能用catch令牌来对抗他们的攻击!包括空指针,以后我就不怕他们了!
可是,他为什么要给我,看他刚才的样子都想打我了,又突然给了我这些?还有他一直在说的规则之力又是什么?这座城市为什么又这么诡异?
>转载链接:https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg
---
category:
- Java核心
tag:
- Java
---
# Java异常处理的20个最佳实践
“三妹啊,今天我来给你传授几个异常处理的最佳实践经验,以免你以后在开发中采坑。”我面带着微笑对三妹说。
......@@ -204,11 +211,6 @@ public int checkReturn() {
“好吧。”三妹无奈地叹了口气。
----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
......
“二哥,你能告诉我 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 中。
![](https://cdn.jsdelivr.net/gh/itwanger/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 关键字每次只能抛出一个异常。
“三妹,这下子清楚了吧?”我抬抬头,看了看三妹说。
“好的,二哥,这下彻底记住了,你真棒!”
\ No newline at end of file
“二哥,[上一节](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` 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。
“三妹,来看一下源码的文档注释就全明白了!”
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/exception/try-catch-finally-01.png)
至于参数 status 的值也很好理解,如果是异常退出,设置为非 0 即可,通常用 1 来表示;如果是想正常退出程序,用 0 表示即可。
“好了,三妹,关于 try-catch-finally 我们就讲到这吧!”我说。
“好的,二哥,已经很清楚了,我很期待下一节能讲 try-with-resources。”哈哈哈哈,三妹已经学会提新要求了,这令我感到非常的开心。
“没问题,下期见~”
---
category:
- Java核心
tag:
- Java
---
# 详解Java7新增的try-with-resouces语法
“二哥,终于等到你讲 try-with-resouces 了!”三妹夸张的表情让我有些吃惊。
......
......@@ -19,6 +19,7 @@ title: Java核心&Java企业级开发&Java面试
<img src="https://img.shields.io/badge/计算机经典电子书-下载-green.svg" alt="无套路下载">
</a>
</p>
一份通俗易懂、风趣幽默的 Java 学习指南,内容涵盖 Java 基础、Java 并发编程、JVM、Java 企业级开发(Git、Spring Boot、MySQL)等知识点。
......@@ -76,111 +77,100 @@ title: Java核心&Java企业级开发&Java面试
### Java概述
- [什么是 Java?](overview/what-is-java.md)
- [Java 的发展简史](overview/java-history.md)
- [Java 的优势](overview/java-advantage.md)
- [JDK 和 JRE 有什么区别?](overview/jdk-jre.md)
- [手把手教你安装集成开发环境 Intellij IDEA](overview/idea.md)
- [第一个 Java 程序:Hello World](overview/hello-world.md)
- [什么是Java?Java发展简史,Java的优势](overview/what-is-java.md)
- [JDK和JRE有什么区别?](overview/jdk-jre.md)
- [安装集成开发环境Intellij IDEA](overview/idea.md)
- [第一个Java程序:Hello World](overview/hello-world.md)
### Java基础语法
- [基本数据类型](basic-grammar/basic-data-type.md)
- [流程控制](basic-grammar/flow-control.md)
- [运算符](basic-grammar/operator.md)
- [注释](basic-grammar/javadoc.md)
### 面向对象
- [什么是对象?什么是类](oo/object-class.md)
- [变量](oo/var.md)
- [方法](oo/method.md)
- [构造方法](oo/construct.md)
- [代码初始化块](oo/code-init.md)
- [抽象类](oo/abstract.md)
- [接口](oo/interface.md)
- [static 关键字](oo/static.md)
- [this 和 super 关键字](oo/this-super.md)
- [final 关键字](oo/final.md)
- [instanceof 关键字](oo/instanceof.md)
- [不可变对象](basic-extra-meal/immutable.md)
- [可变参数](basic-extra-meal/varables.md)
- [泛型](basic-extra-meal/generic.md)
- [注解](basic-extra-meal/annotation.md)
- [枚举](basic-extra-meal/enum.md)
- [反射](basic-extra-meal/fanshe.md)
### 字符串String
- [String 为什么是不可变的?](string/immutable.md)
- [字符串常量池](string/constant-pool.md)
- [深入浅出 String.intern](string/intern.md)
- [如何比较两个字符串是否相等?](string/equals.md)
- [如何拼接字符串?](string/join.md)
- [如何拆分字符串?](string/split.md)
### 数组
- [什么是数组?](array/array.md)
- [如何打印数组?](array/print.md)
- [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中的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)
- [简单介绍下时间复杂度](collection/big-o.md)
- [ArrayList](collection/arraylist.md)
- [LinkedList](collection/linkedlist.md)
- [ArrayList 和 LinkedList 之增删改查的时间复杂度](collection/list-war-1.md)
- [ArrayList 和 LinkedList 的实现方式以及性能对比](collection/list-war-2.md)
- [Java集合框架](collection/gailan.md)
- [Java集合ArrayList详解](collection/arraylist.md)
- [Java集合LinkedList详解](collection/linkedlist.md)
- [Java中ArrayList和LinkedList的区别](collection/list-war-2.md)
- [Iterator与Iterable有什么区别?](collection/iterator-iterable.md)
- [为什么阿里巴巴强制不要在 foreach 里执行删除操作](collection/fail-fast.md)
- [详细讲解 HashMap 的 hash 原理](collection/hash.md)
- [详细讲解 HashMap 的扩容机制](collection/hashmap-resize.md)
- [HashMap 的加载因子为什么是 0.75?](collection/hashmap-loadfactor.md)
- [为什么 HashMap 是线程不安全的?](collection/hashmap-thread-nosafe.md)
- [为什么阿里巴巴强制不要在foreach里执行删除操作](collection/fail-fast.md)
- [Java8系列之重新认识HashMap](collection/hashmap.md)
### Java I/O
- [Java IO学习整理](io/shangtou.md)
- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](io/BIONIOAIO.md)
### 异常处理
- [聊聊异常处理机制](exception/gailan.md)
- [关于 try-catch-finally](exception/try-catch-finally.md)
- [关于 throw 和 throws](exception/throw-throws.md)
- [关于 try-with-resouces](exception/try-with-resouces.md)
- [异常处理机制到底该怎么用?](exception/shijian.md)
- [一文读懂Java异常处理](exception/gailan.md)
- [详解Java7新增的try-with-resouces语法](exception/try-with-resouces.md)
- [Java异常处理的20个最佳实践](exception/shijian.md)
- [Java空指针NullPointerException的传说](exception/npe.md)
### 常用工具类
- [数组工具类:Arrays](common-tool/arrays.md)
- [集合工具类:Collections](common-tool/collections.md)
- [简化每一行代码工具类:Hutool](common-tool/hutool.md)
- [Guava,拯救垃圾代码,效率提升N倍](common-tool/guava.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)
### Java8新特性
### Java新特性
- [入门Java Stream流](https://mp.weixin.qq.com/s/7hNUjjmqKcHDtymsfG_Gtw)
- [Java 8 Optional 最佳指南](https://mp.weixin.qq.com/s/PqK0KNVHyoEtZDtp5odocA)
- [Lambda 表达式入门](https://mp.weixin.qq.com/s/ozr0jYHIc12WSTmmd_vEjw)
- [Java 8 Stream流详细用法](java8/stream.md)
- [Java 8 Optional最佳指南](java8/optional.md)
- [深入浅出Java 8 Lambda表达式](java8/Lambda.md)
### Java重要知识点
- [Java 中常用的 48 个关键字](basic-extra-meal/48-keywords.md)
- [Java 命名的注意事项](basic-extra-meal/java-naming.md)
- [详解 Java 的默认编码方式 Unicode](basic-extra-meal/java-unicode.md)
- [new Integer(18)与Integer.valueOf(18)有什么区别?](basic-extra-meal/int-cache.md)
- [聊聊自动拆箱与自动装箱](basic-extra-meal/box.md)
- [浅拷贝与深拷贝究竟有什么不一样?](basic-extra-meal/deep-copy.md)
- [为什么重写 equals 时必须重写 hashCode 方法?](basic-extra-meal/equals-hashcode.md)
- [方法重载和方法重写有什么区别?](basic-extra-meal/override-overload.md)
- [Java 到底是值传递还是引用传递?](basic-extra-meal/pass-by-value.md)
- [Java 不能实现真正泛型的原因是什么?](basic-extra-meal/true-generic.md)
- [Java 程序在编译期发生了什么?](basic-extra-meal/what-happen-when-javac.md)
- [Comparable和Comparator有什么区别?](basic-extra-meal/comparable-omparator.md)
- [Java IO 流详细划分](io/shangtou.md)
- [如何给女朋友解释什么是 BIO、NIO 和 AIO?](https://mp.weixin.qq.com/s/QQxrr5yP8X9YdFqIwXDoQQ)
- [为什么 Object 类需要一个 hashCode() 方法呢?](https://mp.weixin.qq.com/s/PcbMQ5VGnPXlcgIsK8AW4w)
- [重写的 11 条规则](https://mp.weixin.qq.com/s/tmaK5DSjQhA0IvTrSvKkQQ)
- [空指针的传说](https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg)
- [彻底弄懂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)
### Java并发编程
......@@ -204,6 +194,7 @@ title: Java核心&Java企业级开发&Java面试
- [Java 虚拟机栈](https://mp.weixin.qq.com/s/xaIEqngM-J0DouWYa8Ms7g)
- [JVM 内存区域划分](https://mp.weixin.qq.com/s/NaCFDOGuoHkfQZZjvY66Jg)
- [解剖一下 Java 的 class 文件](https://mp.weixin.qq.com/s/uMEZ2Xwctx4n-_8zvtDp5A)
- [Java程序在编译期发生了什么?](basic-extra-meal/what-happen-when-javac.md)
## Java企业级开发
......@@ -435,12 +426,13 @@ title: Java核心&Java企业级开发&Java面试
### 八股文
- [Java 高频面试题 34 道](baguwen/java-basic-34.md)
- [Java 基础八股文(背诵版)](baguwen/java-basic.md)
- [HashMap 精选面试题](collection/hashmap-interview.md)
- [Java 并发编程八股文(背诵版)](baguwen/java-thread.md)
- [Java 虚拟机八股文(背诵版)](baguwen/jvm.md)
- [Redis 八股文(12 道精选)](mianjing/redis12question.md)
- [Java高频面试题34道](baguwen/java-basic-34.md)
- [Java HashMap精选面试题](collection/hashmap-interview.md)
- [Redis精选面试题](mianjing/redis12question.md)
- [Java基础八股文(背诵版)](baguwen/java-basic.md)
- [Java并发编程八股文(背诵版)](baguwen/java-thread.md)
- [Java虚拟机八股文(背诵版)](baguwen/jvm.md)
### 面试经验
......
---
category:
- Java核心
tag:
- Java
---
# 如何给女朋友解释清楚BIO、NIO和AIO?
周末午后,在家里面进行电话面试,我问了面试者几个关于 IO 的问题,其中包括什么是 BIO、NIO 和 AIO?三者有什么区别?具体如何使用等问题,但是面试者回答的并不是很满意。于是我在面试评价中写道:"对 Java 的 IO 提醒理解不够深入"。恰好被女朋友看到了。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-1)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-2)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-3.gif)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-4)
Java IO
IO,常协作 I/O,是 Input/Output 的简称,即输入/输出。通常指数据在内部存储器(内存)和外部存储器(硬盘、优盘等)或其他周边设备之间的输入和输出。
输入/输出是信息处理系统(例如计算机)与外部世界(可能是人类或另一信息处理系统)之间的通信。
输入是系统接收的信号或数据,输出则是从其发送的信号或数据。
在 Java 中,提供了一系列 API,可以供开发者来读写外部数据或文件。我们称这些 API 为 Java IO。
IO 是 Java 中比较重要,且比较难的知识点,主要是因为随着 Java 的发展,目前有三种 IO 共存。分别是 BIO、NIO 和 AIO。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-5)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-6.gif)
![](https://cdn.jsdelivr.net/gh/itwanger/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 的基础上引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-8)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-9)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-10)
![](https://cdn.jsdelivr.net/gh/itwanger/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 异步:水壶是不是在水烧开之后主动通知人。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-12.gif)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-13)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-14)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-15)
适用场景
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
AIO 方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-16.gif)
![](https://cdn.jsdelivr.net/gh/itwanger/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<Integer> 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<Integer, Object> handler = new CompletionHandler<Integer, Object>() {
@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);
}
}
```
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-18.gif)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-19.gif)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-20.gif)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-21)
滴滴滴,水开了。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-22)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/BIONIOAIO-23)
转载链接:
>https://mp.weixin.qq.com/s/QQxrr5yP8X9YdFqIwXDoQQ
---
category:
- Java核心
tag:
- Java
---
# Java IO学习整理
“老王,Java IO 也太上头了吧?”新兵蛋子小二向头顶很凉快的老王抱怨道,“你瞧,我就按照传输方式对 IO 进行了一个简单的分类,就能搞出来这么多的玩意!”
......
---
category:
- Java核心
tag:
- Java
---
# 深入浅出 Java 8 Lambda表达式
今天分享的主题是《Lambda 表达式入门》,这也是之前一些读者留言强烈要求我写一写的,不好意思,让你们久等了,现在来满足你们,为时不晚吧?
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/java8/Lambda-1)
### 01、初识 Lambda
Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行。考虑下面这段代码:
```java
() -> System.out.println("沉默王二")
```
来从左到右解释一下,`()` 为 Lambda 表达式的参数列表(本例中没有参数),`->` 标识这串代码为 Lambda 表达式(也就是说,看到 `->` 就知道这是 Lambda),`System.out.println("沉默王二")` 为要执行的代码,即将“沉默王二”打印到标准输出流。
有点 Java 基础的同学应该不会对 Runnable 接口感到陌生,这是多线程的一个基础接口,它的定义如下:
```java
@FunctionalInterface
public interface Runnable
{
public abstract void run();
}
```
Runnable 接口非常简单,仅有一个抽象方法 `run()`;细心的同学会发现一个陌生的注解 `@FunctionalInterface`,这个注解是什么意思呢?
我看了它的源码,里面有这样一段注释:
>Note that instances of functional interfaces can be created with lambda expressions, method references, or constructor references.
大致的意思就是说,通过 `@FunctionalInterface` 标记的接口可以通过 Lambda 表达式创建实例。具体怎么表现呢?
原来我们创建一个线程并启动它是这样的:
```java
public class LamadaTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("沉默王二");
}
}).start();
}
}
```
通过 Lambda 表达式呢?只需要下面这样:
```java
public class LamadaTest {
public static void main(String[] args) {
new Thread(() -> System.out.println("沉默王二")).start();
}
}
```
是不是很妙!比起匿名内部类,Lambda 表达式不仅易于理解,更大大简化了必须编写的代码数量。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/java8/Lambda-2)
### 02、Lambda 语法
每个 Lambda 表达式都遵循以下法则:
```
( parameter-list ) -> { expression-or-statements }
```
`()` 中的 `parameter-list` 是以逗号分隔的参数。你可以指定参数的类型,也可以不指定(编译器会根据上下文进行推断)。Java 11 后,还可以使用 `var` 关键字作为参数类型,有点 JavaScript 的味道。
`->` 相当于 Lambda 的标识符,就好像见到圣旨就见到了皇上。
`{}` 中的 `expression-or-statements` 为 Lambda 的主体,可以是一行语句,也可以多行。
可以通过 Lambda 表达式干很多事情,比如说
1)为变量赋值,示例如下:
```java
Runnable r = () -> { System.out.println("沉默王二"); };
r.run();
```
2)作为 return 结果,示例如下:
```java
static FileFilter getFilter(String ext)
{
return (pathname) -> pathname.toString().endsWith(ext);
}
```
3)作为数组元素,示例如下:
```java
final PathMatcher matchers[] =
{
(path) -> path.toString().endsWith("txt"),
(path) -> path.toString().endsWith("java")
};
```
4)作为普通方法或者构造方法的参数,示例如下:
```java
new Thread(() -> System.out.println("沉默王二")).start();
```
需要注意 Lambda 表达式的作用域范围。
```java
public static void main(String[] args) {
int limit = 10;
Runnable r = () -> {
int limit = 5;
for (int i = 0; i < limit; i++)
System.out.println(i);
};
}
```
上面这段代码在编译的时候会提示错误:变量 limit 已经定义过了。
和匿名内部类一样,不要在 Lambda 表达式主体内对方法内的局部变量进行修改,否则编译也不会通过:Lambda 表达式中使用的变量必须是 final 的。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/java8/Lambda-3)
这个问题发生的原因是因为 Java 规范中是这样规定的:
>Any local variable, formal parameter, or exception parameter used but not declared in a lambda expression
must either be declared final or be effectively final [(§4.12.4)](http://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4),
or a compile-time error occurs where the use is attempted.
大致的意思就是说,Lambda 表达式中要用到的,但又未在 Lambda 表达式中声明的变量,必须声明为 final 或者是 effectively final,否则就会出现编译错误。
关于 final 和 effectively final 的区别,可能有些小伙伴不太清楚,这里多说两句。
```java
final int a;
a = 1;
// a = 2;
// 由于 a 是 final 的,所以不能被重新赋值
int b;
b = 1;
// b 此后再未更改
// b 就是 effectively final
int c;
c = 1;
// c 先被赋值为 1,随后又被重新赋值为 2
c = 2;
// c 就不是 effectively final
```
明白了 final 和 effectively final 的区别后,我们了解到,如果把 limit 定义为 final,那就无法在 Lambda 表达式中修改变量的值。那有什么好的解决办法呢?既能让编译器不发出警告,又能修改变量的值。
思前想后,试来试去,我终于找到了 3 个可行的解决方案:
1)把 limit 变量声明为 static。
2)把 limit 变量声明为 AtomicInteger。
3)使用数组。
下面我们来详细地一一介绍下。
#### 01)把 limit 变量声明为 static
要想把 limit 变量声明为 static,就必须将 limit 变量放在 `main()` 方法外部,因为 `main()` 方法本身是 static 的。完整的代码示例如下所示。
```java
public class ModifyVariable2StaticInsideLambda {
static int limit = 10;
public static void main(String[] args) {
Runnable r = () -> {
limit = 5;
for (int i = 0; i < limit; i++) {
System.out.println(i);
}
};
new Thread(r).start();
}
}
```
来看一下程序输出的结果:
```
0
1
2
3
4
```
OK,该方案是可行的。
#### 02)把 limit 变量声明为 AtomicInteger
AtomicInteger 可以确保 int 值的修改是原子性的,可以使用 `set()` 方法设置一个新的 int 值,`get()` 方法获取当前的 int 值。
```java
public class ModifyVariable2AtomicInsideLambda {
public static void main(String[] args) {
final AtomicInteger limit = new AtomicInteger(10);
Runnable r = () -> {
limit.set(5);
for (int i = 0; i < limit.get(); i++) {
System.out.println(i);
}
};
new Thread(r).start();
}
}
```
来看一下程序输出的结果:
```
0
1
2
3
4
```
OK,该方案也是可行的。
#### 03)使用数组
使用数组的方式略带一些欺骗的性质,在声明数组的时候设置为 final,但更改 int 的值时却修改的是数组的一个元素。
```java
public class ModifyVariable2ArrayInsideLambda {
public static void main(String[] args) {
final int [] limits = {10};
Runnable r = () -> {
limits[0] = 5;
for (int i = 0; i < limits[0]; i++) {
System.out.println(i);
}
};
new Thread(r).start();
}
}
```
来看一下程序输出的结果:
```
0
1
2
3
4
```
OK,该方案也是可行的。
### 03、Lambda 和 this 关键字
Lambda 表达式并不会引入新的作用域,这一点和匿名内部类是不同的。也就是说,Lambda 表达式主体内使用的 this 关键字和其所在的类实例相同。
来看下面这个示例。
```java
public class LamadaTest {
public static void main(String[] args) {
new LamadaTest().work();
}
public void work() {
System.out.printf("this = %s%n", this);
Runnable r = new Runnable()
{
@Override
public void run()
{
System.out.printf("this = %s%n", this);
}
};
new Thread(r).start();
new Thread(() -> System.out.printf("this = %s%n", this)).start();
}
}
```
Tips:`%s` 代表当前位置输出字符串,`%n` 代表换行符,也可以使用 `\n` 代替,但 `%n` 是跨平台的。
`work()` 方法中的代码可以分为 3 个部分:
1)单独的 this 关键字
```java
System.out.printf("this = %s%n", this);
```
其中 this 为 `main()` 方法中通过 new 关键字创建的 LamadaTest 对象——`new LamadaTest()`
2)匿名内部类中的 this 关键字
```java
Runnable r = new Runnable()
{
@Override
public void run()
{
System.out.printf("this = %s%n", this);
}
};
```
其中 this 为 `work()` 方法中通过 new 关键字创建的 Runnable 对象——`new Runnable(){...}`
3)Lambda 表达式中的 this 关键字
其中 this 关键字和 1)中的相同。
我们来看一下程序的输出结果:
```java
this = com.cmower.java_demo.journal.LamadaTest@3feba861
this = com.cmower.java_demo.journal.LamadaTest$1@64f033cb
this = com.cmower.java_demo.journal.LamadaTest@3feba861
```
符合我们分析的预期。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/java8/Lambda-4)
### 04、最后
尽管 Lambda 表达式在简化 Java 编程方面做了很多令人惊讶的努力,但在某些情况下,不当的使用仍然会导致不必要的混乱,大家伙慎用。
好了,我亲爱的读者朋友们,以上就是本文的全部内容了。能在疫情期间坚持看技术文,二哥必须要伸出大拇指为你点个赞👍。原创不易,如果觉得有点用的话,请不要吝啬你手中**点赞**的权力——因为这将是我写作的最强动力。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/java8/Lambda-5.png)
---
category:
- Java核心
tag:
- Java
date: 2019-01-01
---
# Java 8 Optional最佳指南
想学习,永远都不晚,尤其是针对 Java 8 里面的好东西,Optional 就是其中之一,该类提供了一种用于表示可选值而非空引用的类级别解决方案。作为一名 Java 程序员,我真的是烦透了 NullPointerException(NPE),尽管和它熟得就像一位老朋友,知道它也是迫不得已——程序正在使用一个对象却发现这个对象的值为 null,于是 Java 虚拟机就怒发冲冠地把它抛了出来当做替罪羊。
当然了,我们程序员是富有责任心的,不会坐视不管,于是就有了大量的 null 值检查。尽管有时候这种检查完全没有必要,但我们已经习惯了例行公事。终于,Java 8 看不下去了,就引入了 Optional,以便我们编写的代码不再那么刻薄呆板。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/java8/optional-1)
### 01、没有 Optional 会有什么问题
我们来模拟一个实际的应用场景。小王第一天上班,领导老马就给他安排了一个任务,要他从数据库中根据会员 ID 拉取一个会员的姓名,然后将姓名打印到控制台。虽然是新来的,但这个任务难不倒小王,于是他花了 10 分钟写下了这段代码:
```java
public class WithoutOptionalDemo {
class Member {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) {
Member mem = getMemberByIdFromDB();
if (mem != null) {
System.out.println(mem.getName());
}
}
public static Member getMemberByIdFromDB() {
// 当前 ID 的会员不存在
return null;
}
}
```
由于当前 ID 的会员不存在,所以 `getMemberByIdFromDB()` 方法返回了 null 来作为没有获取到该会员的结果,那就意味着在打印会员姓名的时候要先对 mem 判空,否则就会抛出 NPE 异常,不信?让小王把 `if (mem != null)` 去掉试试,控制台立马打印错误堆栈给你颜色看看。
```
Exception in thread "main" java.lang.NullPointerException
at com.cmower.dzone.optional.WithoutOptionalDemo.main(WithoutOptionalDemo.java:24)
```
### 02、Optional 是如何解决这个问题的
小王把代码提交后,就兴高采烈地去找老马要新的任务了。本着虚心学习的态度,小王请求老马看一下自己的代码,于是老王就告诉他应该尝试一下 Optional,可以避免没有必要的 null 值检查。现在,让我们来看看小王是如何通过 Optional 来解决上述问题的。
```java
public class OptionalDemo {
public static void main(String[] args) {
Optional<Member> optional = getMemberByIdFromDB();
optional.ifPresent(mem -> {
System.out.println("会员姓名是:" + mem.getName());
});
}
public static Optional<Member> getMemberByIdFromDB() {
boolean hasName = true;
if (hasName) {
return Optional.of(new Member("沉默王二"));
}
return Optional.empty();
}
}
class Member {
private String name;
public String getName() {
return name;
}
// getter / setter
}
```
`getMemberByIdFromDB()` 方法返回了 `Optional<Member>` 作为结果,这样就表明 Member 可能存在,也可能不存在,这时候就可以在 Optional 的 `ifPresent()` 方法中使用 Lambda 表达式来直接打印结果。
Optional 之所以可以解决 NPE 的问题,是因为它明确的告诉我们,不需要对它进行判空。它就好像十字路口的路标,明确地告诉你该往哪走。
### 03、创建 Optional 对象
1)可以使用静态方法 `empty()` 创建一个空的 Optional 对象
```java
Optional<String> empty = Optional.empty();
System.out.println(empty); // 输出:Optional.empty
```
2)可以使用静态方法 `of()` 创建一个非空的 Optional 对象
```java
Optional<String> opt = Optional.of("沉默王二");
System.out.println(opt); // 输出:Optional[沉默王二]
```
当然了,传递给 `of()` 方法的参数必须是非空的,也就是说不能为 null,否则仍然会抛出 NullPointerException。
```java
String name = null;
Optional<String> optnull = Optional.of(name);
```
3)可以使用静态方法 `ofNullable()` 创建一个即可空又可非空的 Optional 对象
```java
String name = null;
Optional<String> optOrNull = Optional.ofNullable(name);
System.out.println(optOrNull); // 输出:Optional.empty
```
`ofNullable()` 方法内部有一个三元表达式,如果为参数为 null,则返回私有常量 EMPTY;否则使用 new 关键字创建了一个新的 Optional 对象——不会再抛出 NPE 异常了。
### 04、判断值是否存在
可以通过方法 `isPresent()` 判断一个 Optional 对象是否存在,如果存在,该方法返回 true,否则返回 false——取代了 `obj != null` 的判断。
```java
Optional<String> opt = Optional.of("沉默王二");
System.out.println(opt.isPresent()); // 输出:true
Optional<String> optOrNull = Optional.ofNullable(null);
System.out.println(opt.isPresent()); // 输出:false
```
Java 11 后还可以通过方法 `isEmpty()` 判断与 `isPresent()` 相反的结果。
```java
Optional<String> opt = Optional.of("沉默王二");
System.out.println(opt.isPresent()); // 输出:false
Optional<String> optOrNull = Optional.ofNullable(null);
System.out.println(opt.isPresent()); // 输出:true
```
### 05、非空表达式
Optional 类有一个非常现代化的方法——`ifPresent()`,允许我们使用函数式编程的方式执行一些代码,因此,我把它称为非空表达式。如果没有该方法的话,我们通常需要先通过 `isPresent()` 方法对 Optional 对象进行判空后再执行相应的代码:
```java
Optional<String> optOrNull = Optional.ofNullable(null);
if (optOrNull.isPresent()) {
System.out.println(optOrNull.get().length());
}
```
有了 `ifPresent()` 之后,情况就完全不同了,可以直接将 Lambda 表达式传递给该方法,代码更加简洁,更加直观。
```java
Optional<String> opt = Optional.of("沉默王二");
opt.ifPresent(str -> System.out.println(str.length()));
```
Java 9 后还可以通过方法 `ifPresentOrElse(action, emptyAction)` 执行两种结果,非空时执行 action,空时执行 emptyAction。
```java
Optional<String> opt = Optional.of("沉默王二");
opt.ifPresentOrElse(str -> System.out.println(str.length()), () -> System.out.println("为空"));
```
### 06、设置(获取)默认值
有时候,我们在创建(获取) Optional 对象的时候,需要一个默认值,`orElse()``orElseGet()` 方法就派上用场了。
`orElse()` 方法用于返回包裹在 Optional 对象中的值,如果该值不为 null,则返回;否则返回默认值。该方法的参数类型和值得类型一致。
```java
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("沉默王二");
System.out.println(name); // 输出:沉默王二
```
`orElseGet()` 方法与 `orElse()` 方法类似,但参数类型不同。如果 Optional 对象中的值为 null,则执行参数中的函数。
```java
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(()->"沉默王二");
System.out.println(name); // 输出:沉默王二
```
从输出结果以及代码的形式上来看,这两个方法极其相似,这不免引起我们的怀疑,Java 类库的设计者有必要这样做吗?
假设现在有这样一个获取默认值的方法,很传统的方式。
```java
public static String getDefaultValue() {
System.out.println("getDefaultValue");
return "沉默王二";
}
```
然后,通过 `orElse()` 方法和 `orElseGet()` 方法分别调用 `getDefaultValue()` 方法返回默认值。
```java
public static void main(String[] args) {
String name = null;
System.out.println("orElse");
String name2 = Optional.ofNullable(name).orElse(getDefaultValue());
System.out.println("orElseGet");
String name3 = Optional.ofNullable(name).orElseGet(OrElseOptionalDemo::getDefaultValue);
}
```
注:`类名 :: 方法名`是 Java 8 引入的语法,方法名后面是没有 `()` 的,表明该方法并不一定会被调用。
输出结果如下所示:
```java
orElse
getDefaultValue
orElseGet
getDefaultValue
```
输出结果是相似的,没什么太大的不同,这是在 Optional 对象的值为 null 的情况下。假如 Optional 对象的值不为 null 呢?
```java
public static void main(String[] args) {
String name = "沉默王三";
System.out.println("orElse");
String name2 = Optional.ofNullable(name).orElse(getDefaultValue());
System.out.println("orElseGet");
String name3 = Optional.ofNullable(name).orElseGet(OrElseOptionalDemo::getDefaultValue);
}
```
输出结果如下所示:
```java
orElse
getDefaultValue
orElseGet
```
咦,`orElseGet()` 没有去调用 `getDefaultValue()`。哪个方法的性能更佳,你明白了吧?
### 07、获取值
直观从语义上来看,`get()` 方法才是最正宗的获取 Optional 对象值的方法,但很遗憾,该方法是有缺陷的,因为假如 Optional 对象的值为 null,该方法会抛出 NoSuchElementException 异常。这完全与我们使用 Optional 类的初衷相悖。
```java
public class GetOptionalDemo {
public static void main(String[] args) {
String name = null;
Optional<String> optOrNull = Optional.ofNullable(name);
System.out.println(optOrNull.get());
}
}
```
这段程序在运行时会抛出异常:
```
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.get(Optional.java:141)
at com.cmower.dzone.optional.GetOptionalDemo.main(GetOptionalDemo.java:9)
```
尽管抛出的异常是 NoSuchElementException 而不是 NPE,但在我们看来,显然是在“五十步笑百步”。建议 `orElseGet()` 方法获取 Optional 对象的值。
### 08、过滤值
小王通过 Optional 类对之前的代码进行了升级,完成后又兴高采烈地跑去找老马要任务了。老马觉得这小伙子不错,头脑灵活,又干活积极,很值得培养,就又交给了小王一个新的任务:用户注册时对密码的长度进行检查。
小王拿到任务后,乐开了花,因为他刚要学习 Optional 类的 `filter()` 方法,这就派上了用场。
```java
public class FilterOptionalDemo {
public static void main(String[] args) {
String password = "12345";
Optional<String> opt = Optional.ofNullable(password);
System.out.println(opt.filter(pwd -> pwd.length() > 6).isPresent());
}
}
```
`filter()` 方法的参数类型为 Predicate(Java 8 新增的一个函数式接口),也就是说可以将一个 Lambda 表达式传递给该方法作为条件,如果表达式的结果为 false,则返回一个 EMPTY 的 Optional 对象,否则返回过滤后的 Optional 对象。
在上例中,由于 password 的长度为 5 ,所以程序输出的结果为 false。假设密码的长度要求在 6 到 10 位之间,那么还可以再追加一个条件。来看小王增加难度后的代码。
```java
Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
password = "1234567";
opt = Optional.ofNullable(password);
boolean result = opt.filter(len6.and(len10)).isPresent();
System.out.println(result);
```
这次程序输出的结果为 true,因为密码变成了 7 位,在 6 到 10 位之间。想象一下,假如小王使用 if-else 来完成这个任务,代码该有多冗长。
### 09、转换值
小王检查完了密码的长度,仍然觉得不够尽兴,觉得要对密码的强度也进行检查,比如说密码不能是“password”,这样的密码太弱了。于是他又开始研究起了 `map()` 方法,该方法可以按照一定的规则将原有 Optional 对象转换为一个新的 Optional 对象,原有的 Optional 对象不会更改。
先来看小王写的一个简单的例子:
```java
public class OptionalMapDemo {
public static void main(String[] args) {
String name = "沉默王二";
Optional<String> nameOptional = Optional.of(name);
Optional<Integer> intOpt = nameOptional
.map(String::length);
System.out.println( intOpt.orElse(0));
}
}
```
在上面这个例子中,`map()` 方法的参数 `String::length`,意味着要 将原有的字符串类型的 Optional 按照字符串长度重新生成一个新的 Optional 对象,类型为 Integer。
搞清楚了 `map()` 方法的基本用法后,小王决定把 `map()` 方法与 `filter()` 方法结合起来用,前者用于将密码转化为小写,后者用于判断长度以及是否是“password”。
```java
public class OptionalMapFilterDemo {
public static void main(String[] args) {
String password = "password";
Optional<String> opt = Optional.ofNullable(password);
Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
Predicate<String> eq = pwd -> pwd.equals("password");
boolean result = opt.map(String::toLowerCase).filter(len6.and(len10 ).and(eq)).isPresent();
System.out.println(result);
}
}
```
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/java8/optional-2)
好了,我亲爱的读者朋友,以上就是本文的全部内容了——可以说是史上最佳 Optional 指南了,能看到这里的都是最优秀的程序员,二哥必须要伸出大拇指为你点个赞。
---
category:
- Java核心
tag:
- Java
date: 2019-01-01
---
# Java 8 Stream流详细用法
两个星期以前,就有读者强烈要求我写一篇 Java Stream 流的文章,我说市面上不是已经有很多了吗,结果你猜他怎么说:“就想看你写的啊!”你看你看,多么苍白的喜欢啊。那就“勉为其难”写一篇吧,嘻嘻。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/java8/stream-1)
单从“Stream”这个单词上来看,它似乎和 java.io 包下的 InputStream 和 OutputStream 有些关系。实际上呢,没毛关系。Java 8 新增的 Stream 是为了解放程序员操作集合(Collection)时的生产力,之所以能解放,很大一部分原因可以归功于同时出现的 Lambda 表达式——极大的提高了编程效率和程序可读性。
Stream 究竟是什么呢?
>Stream 就好像一个高级的迭代器,但只能遍历一次,就好像一江春水向东流;在流的过程中,对流中的元素执行一些操作,比如“过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等。
要想操作流,首先需要有一个数据源,可以是数组或者集合。每次操作都会返回一个新的流对象,方便进行链式操作,但原有的流对象会保持不变。
流的操作可以分为两种类型:
1)中间操作,可以有多个,每次返回一个新的流,可进行链式操作。
2)终端操作,只能有一个,每次执行完,这个流也就用光光了,无法执行下一个操作,因此只能放在最后。
来举个例子。
```java
List<String> list = new ArrayList<>();
list.add("武汉加油");
list.add("中国加油");
list.add("世界加油");
list.add("世界加油");
long count = list.stream().distinct().count();
System.out.println(count);
```
`distinct()` 方法是一个中间操作(去重),它会返回一个新的流(没有共同元素)。
```java
Stream<T> 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<String> stream = Arrays.stream(arr);
stream = Stream.of("武汉加油", "中国加油", "世界加油");
List<String> list = new ArrayList<>();
list.add("武汉加油");
list.add("中国加油");
list.add("世界加油");
stream = list.stream();
}
}
```
查看 Stream 源码的话,你会发现 `of()` 方法内部其实调用了 `Arrays.stream()` 方法。
```java
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
```
另外,集合还可以调用 `parallelStream()` 方法创建并发流,默认使用的是 `ForkJoinPool.commonPool()`线程池。
```java
List<Long> aList = new ArrayList<>();
Stream<Long> parallelStream = aList.parallelStream();
```
### 02、操作流
Stream 类提供了很多有用的操作流的方法,我来挑一些常用的给你介绍一下。
1)过滤
通过 `filter()` 方法可以从流中筛选出我们想要的元素。
```java
public class FilterStreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("周杰伦");
list.add("王力宏");
list.add("陶喆");
list.add("林俊杰");
Stream<String> 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<String> list = new ArrayList<>();
list.add("周杰伦");
list.add("王力宏");
list.add("陶喆");
list.add("林俊杰");
Stream<Integer> stream = list.stream().map(String::length);
stream.forEach(System.out::println);
}
}
```
`map()` 方法接收的是一个 Function(Java 8 新增的一个函数式接口,接受一个输入参数 T,返回一个结果 R)类型的参数,此时参数 为 String 类的 length 方法,也就是把 `Stream<String>` 的流转成一个 `Stream<Integer>` 的流。
程序输出的结果如下所示:
```
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<String> 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<T> reduce(BinaryOperator<T> accumulator)`
没有起始值,只有一个参数,就是运算规则,此时返回 [Optional](https://mp.weixin.qq.com/s/PqK0KNVHyoEtZDtp5odocA)
- `T reduce(T identity, BinaryOperator<T> accumulator)`
有起始值,有运算规则,两个参数,此时返回的类型和起始值类型一致。
来看下面这个例子。
```java
public class ReduceStreamDemo {
public static void main(String[] args) {
Integer[] ints = {0, 1, 2, 3};
List<Integer> list = Arrays.asList(ints);
Optional<Integer> optional = list.stream().reduce((a, b) -> a + b);
Optional<Integer> 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<String> 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<Integer> list1 = list.stream().map(String::length).collect(Collectors.toList());
List<String> 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> A[] toArray(IntFunction<A[]> 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<Integer> 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]
[周杰伦, 王力宏, 陶喆, 林俊杰]
周杰伦, 王力宏, 陶喆, 林俊杰
```
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/java8/stream-2)
---
category:
- Java核心
tag:
- Java
---
# Java抽象类
“二哥,你这明显加快了更新的频率呀!”三妹对于我最近的肝劲由衷的佩服了起来。
......
---
category:
- Java核心
tag:
- Java
---
# Java代码初始化块
“哥,今天我们要学习的内容是‘代码初始化块’,对吧?”看来三妹已经提前预习了我上次留给她的作业。
......
---
category:
- Java核心
tag:
- Java
---
# Java构造方法
我对三妹说,“[上一节](https://mp.weixin.qq.com/s/L4jAgQPurGZPvWu8ECtBpA)学了 Java 中的方法,接着学构造方法的话,难度就小很多了。”
......
---
category:
- Java核心
tag:
- Java
---
# 浅析Java中的final关键字
“哥,今天学什么呢?”
......
---
category:
- Java核心
tag:
- Java
---
# Java instanceof关键字用法
instanceof 关键字的用法其实很简单:
......@@ -131,10 +138,4 @@ if (obj instanceof String s) {
“哇,这样就简洁了呀!”三妹不仅惊叹到!
好了,关于 instanceof 操作符我们就先讲到这吧,难是一点都不难,希望各位同学也能够很好的掌握。
-----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
\ No newline at end of file
好了,关于 instanceof 操作符我们就先讲到这吧,难是一点都不难,希望各位同学也能够很好的掌握。
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# Java接口
“哥,我看你朋友圈说《Java 程序员进阶之路》专栏收到了第一笔赞赏呀,虽然只有一块钱,但我也替你感到开心。”三妹的脸上洋溢着自信的微笑,仿佛这钱是打给她的一样。
......@@ -314,14 +321,3 @@ for (Shape shape : shapes) {
接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系,举个例子来说,所有的类都可以实现 `Serializable` 接口,从而具有序列化的功能,但不能说所有的类和 Serializable 之间是 `is-a` 的关系。
--------
“好了,三妹,接口就学到这吧,下课,哈哈哈。”我抬起头看了看窗外,天气还真不错,希望五一的张家界也能晴空万里~
“嗯嗯,哥,休息下吧,我给你揉揉肩膀~~~~”不得不说,有个贴心的妹妹还真的是挺舒服。。。。。
-----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# Java方法
“二哥,[上一节](https://mp.weixin.qq.com/s/UExby8GP3kSacCXliQw8pQ)学了对象和类,这一节我们学什么呢?”三妹满是期待的问我。
......
---
category:
- Java核心
tag:
- Java
---
# 怎么理解Java中类和对象的概念?
“二哥,我那天在图书馆复习[上一节](https://mp.weixin.qq.com/s/WzMEOEdzI0fFwBQ4s0S-0g)你讲的内容,刚好碰见一个学长,他问我有没有‘对象’,我说还没有啊。结果你猜他说什么,‘要不要我给你 new 一个啊?’我当时就懵了,new 是啥意思啊,二哥?”三妹满是疑惑的问我。
......
---
category:
- Java核心
tag:
- Java
---
# Java中的static关键字解析
“哥,你牙龈肿痛轻点没?周一的教妹学 Java 你都没有更新,偷懒了呀!”三妹关心地问我。
......@@ -138,7 +145,7 @@ public class StaticCounter {
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/19-03.png)
### 02、 静态方法
### 02、静态方法
“说完静态变量,我们来说静态方法。”说完,我准备点一支华子来抽,三妹阻止了我,她指一指烟盒上的「吸烟有害身体健康」,我笑了。
......
---
category:
- Java核心
tag:
- Java
---
# Java中this和super的用法总结
“哥,被喊大舅子的感觉怎么样啊?”三妹不怀好意地对我说,她眼睛里充满着不屑。
......
---
category:
- Java核心
tag:
- Java
---
# Java变量的作用域:局部变量、成员变量、静态变量、常量
“二哥,听说 Java 变量在以后的日子里经常用,能不能提前给我透露透露?”三妹咪了一口麦香可可奶茶后对我说。
......
---
category:
- Java核心
tag:
- Java
date: 2019-01-01
---
# 第一个Java程序:Hello World
可以通过 Intellij IDEA 来编写代码,也可以使用在线编辑器来完成。
## 一、安装集成开发环境Intellij IDEA
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/helloworld-01.png)
IntelliJ IDEA 简称 IDEA,是业界公认为最好的 Java 集成开发工具,尤其是在代码自动提示、代码重构、代码版本管理、单元测试、代码分析等方面有着亮眼的发挥。
IDEA 产于捷克,开发人员以严谨著称的东欧程序员为主,分为社区版和付费版两个版本。我们在学习阶段,社区版就足够用了。
回想起我最初学 Java 的时候,老师要求我们在记事本上敲代码,在命令行中编译和执行 Java 代码,搞得全班三分之二的同学都做好了放弃学习 Java 的打算。
鉴于此,我强烈推荐大家使用集成开发工具,比如说 IntelliJ IDEA 来学习。
IDEA 分为社区版和付费版两个版本。
### 01、下载 IDEA
IntelliJ IDEA 的官方下载地址为:[https://www.jetbrains.com/idea/download/](https://www.jetbrains.com/idea/download)
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-1.png)
UItimate 为付费版,可以免费试用,主要针对的是 Web 和企业开发用户;Community 为免费版,可以免费使用,主要针对的是 Java 初学者和安卓开发用户。
功能上的差别如下图所示。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-2.png)
本篇教程主要针对的是 Java 初学者,所以选择免费版为例,点击「Download」进行下载。
稍等一分钟时间,大概 580M。
### 02、安装 IDEA
双击运行 IDEA 安装程序,一步步傻瓜式的下一步就行了。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-3.png)
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-4.png)
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-5.png)
为了方便启动 IDEA,可以勾选【64-bit launcher】复选框。为了关联 Java 源文件,可以勾选【.java】复选框。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-6.png)
点击【Install】后,需要静静地等待一会,大概一分钟的时间,趁机休息一下眼睛。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-7.png)
安装完成后的界面如下图所示。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-8.png)
### 03、启动 IDEA
回到桌面,双击运行 IDEA 的快捷方式,启动 IDEA。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-9.png)
假装阅读完条款后,勾选同意复选框,点击【Continue】
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-10.png)
如果想要帮助 IDEA 收集改进信息,可以点击【Send Usage Statistics】;否则点击【Don't send】。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-11.png)
到此,Intellij IDEA 的安装就完成了,很简单。
## 二、Hello World
第一个 Java 程序非常简单,代码如下:
......@@ -45,6 +121,105 @@ IDEA 会自动保存,在代码编辑面板中右键,在弹出的菜单中选
- `System.out.println()`:一个 Java 语句,一般情况下是将传递的参数打印到控制台。System 是 java.lang 包中的一个 final 类,该类提供的设施包括标准输入,标准输出和错误输出流等等。out 是 System 类的静态成员字段,类型为 PrintStream,它与主机的标准输出控制台进行映射。println 是 PrintStream 类的一个方法,通过调用 print 方法并添加一个换行符实现的。
“三妹,怎么样?这下没有困扰你的关键字了吧?后面我们更细致地分析这些关键字,所以担心是大可不必的。”
## 三、JDK和JRE有什么区别?
### 01、JDK
JDK 是 Java Development Kit 的首字母缩写,是提供给 Java 程序员的开发工具包,换句话说,没有 JDK,Java 程序员就无法使用 Java 语言编写 Java 程序。也就是说,JDK 是用于开发 Java 程序的最小环境。
想要成为一名 Java 程序员,首先就需要在电脑上安装 JDK。当然了,新版的 Intellij IDEA(公认最好用的集成开发环境)已经支持直接下载 JDK 了。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-01.png)
并且支持下载不同版本的 JDK,除了 Oracle 的 OpenJDK,还有社区维护版 AdoptOpenJDK,里面包含了目前使用范围最广的 HotSpot 虚拟机。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-02.png)
如果下载比较慢的话,可以直接在 AdoptOpenJDK 官网上下载。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-03.png)
如果还是比较慢的话,通过 Oracle 官网下载吧!
>https://www.oracle.com/java/technologies/javase-jdk11-downloads.html
JDK 安装成功后,就可以编写 Java 代码了,小伙伴们可以参照上一篇文章《[Hello World](https://mp.weixin.qq.com/s/GYDFndO0Q1Nqzcc_Te61gw)》。
JDK 包含了 JRE,同时还包含了编译 Java 源码的编译器 javac,以及其他的一些重要工具:
- keytool:用于操作 keystore 密钥;
- javap:class 类文件的最基础的反编译器;
- jstack:用于打印 Java 线程栈踪迹的工具;
- jconsole:用于监视 Java 程序的工具;
- jhat:用于 Java 堆分析的工具
- jar:用于打包 Java 程序的工具;
- javadoc:用于生成 Java 文档的工具;
### 02、JRE
JRE 是 Java Runtime Environment 的首字母缩写,是提供给 Java 程序运行的最小环境,换句话说,没有 JRE,Java 程序就无法运行。
Java 程序运行的正式环境一般会选择 Linux 服务器,因为更安全、更高效、更稳定。我们只需要在 Linux 服务器上安装 JRE 就可以运行 Java 程序,而不必安装 JDK,因为我们不需要在服务器上编译和调试 Java 源代码。
刚好我有一台闲置的阿里云服务器,这里就给小伙伴们演示一下 JRE 的安装过程。
第一步:使用以下命令列出服务器上可以安装的 Java 环境:
>yum list java*
可以看到有这么一些(只列出 Java 11 的部分——最近一个 LTS 版本):
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-04.png)
其中 JRE 为 java-11-openjdk.x86_64,JDK 为 java-11-openjdk-devel.x86_64。
第二步,使用以下命令安装 JRE:
>yum install java-11-openjdk.x86_64
第三步,使用以下命令测试是否安装成功:
>java -version
如果出现以下结果,则表明安装成功:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-05.png)
由于 JRE 中不包含 javac,所以 `javac -version` 的结果如下所示:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-06.png)
那既然服务器上的 JRE 环境已经 OK 了,那我们就把之前的“Hello World”程序打成 jar 上传过去,让它跑起来。
第一步,Maven clean(对项目清理):
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-07.png)
第二步,Maven package(对项目打包):
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-08.png)
可以在 Run 面板中看到以下信息:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-09.png)
说明项目打包成功了。
第三步,使用 FileZilla 工具将 jar 包上传到服务器指定目录。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-10.png)
第四步,使用 iTerm2 工具连接服务器。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-11.png)
第五步,执行以下命令:
>java -cp TechSister-1.0-SNAPSHOT.jar com.itwanger.five.HelloWorld
可以看到以下结果:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-12.png)
“没有了,二哥,好期待后面的内容哦!”
\ No newline at end of file
“搞定了,三妹,今天我们就学到这吧。”转动了一下僵硬的脖子后,我对三妹说,“开发环境需要安装 JDK,因为既需要编写源代码,还需要打包和测试;生产环境只需要安装 JRE,因为只需要运行编译打包好的 jar 包即可。”
\ No newline at end of file
IntelliJ IDEA 简称 IDEA,是业界公认为最好的 Java 集成开发工具,尤其是在代码自动提示、代码重构、代码版本管理、单元测试、代码分析等方面有着亮眼的发挥。
IDEA 产于捷克,开发人员以严谨著称的东欧程序员为主,分为社区版和付费版两个版本。我们在学习阶段,社区版就足够用了。
回想起我最初学 Java 的时候,老师要求我们在记事本上敲代码,在命令行中编译和执行 Java 代码,搞得全班三分之二的同学都做好了放弃学习 Java 的打算。
鉴于此,我强烈推荐大家使用集成开发工具,比如说 IntelliJ IDEA 来学习。
IDEA 分为社区版和付费版两个版本。
(2019 年时出的教程,新版的安装和之前一样)
### 01、下载 IDEA
IntelliJ IDEA 的官方下载地址为:[https://www.jetbrains.com/idea/download/](https://www.jetbrains.com/idea/download)
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-1.png)
UItimate 为付费版,可以免费试用,主要针对的是 Web 和企业开发用户;Community 为免费版,可以免费使用,主要针对的是 Java 初学者和安卓开发用户。
功能上的差别如下图所示。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-2.png)
本篇教程主要针对的是 Java 初学者,所以选择免费版为例,点击「Download」进行下载。
稍等一分钟时间,大概 580M。
### 02、安装 IDEA
双击运行 IDEA 安装程序,一步步傻瓜式的下一步就行了。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-3.png)
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-4.png)
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-5.png)
为了方便启动 IDEA,可以勾选【64-bit launcher】复选框。为了关联 Java 源文件,可以勾选【.java】复选框。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-6.png)
点击【Install】后,需要静静地等待一会,大概一分钟的时间,趁机休息一下眼睛。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-7.png)
安装完成后的界面如下图所示。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-8.png)
### 03、启动 IDEA
回到桌面,双击运行 IDEA 的快捷方式,启动 IDEA。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-9.png)
假装阅读完条款后,勾选同意复选框,点击【Continue】
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-10.png)
如果想要帮助 IDEA 收集改进信息,可以点击【Send Usage Statistics】;否则点击【Don't send】。
![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-11.png)
到此,Intellij IDEA 的安装就完成了,很简单。
\ No newline at end of file
尽管 Java 已经 25 岁了,但仍然“宝刀未老”。在 Stack Overflow 2019 年流行编程语言调查报告中,Java 位居第 5 位,有 41% 的受调开发者认为 Java 仍然是一门受欢迎的编程语言。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/three-01.png)
很多大型的互联网公司都在使用 Java,国内最有名的当属阿里巴巴,国外最有名的当属谷歌。那为什么 Java 如此流行呢?
**1)简单性**
Java 为开发者提供了简单易用的用户体验,与其他面向对象编程语言相比,Java 的设计和生态库具有巨大的优势。Java 剔除了 C++ 中很少使用、难以理解、易混淆的特别,比如说指针运算、操作符重载,内存管理等。
Java 可以做到堆栈分配、垃圾回收和自动内存管理,在一定程度上为开发者减轻了入门的难度。
**2)可移植性**
如果 Java 直接编译成操作系统能识的二进制码,可能一个标识在 Windows 操作系统下是1100,而 Linux 下是 1001,这样的话,在 Windows 操作系统下可以运行的程序到了 Linux 环境下就无法运行。
为了解决这个问题,Java 先编译生成字节码,再由 JVM(Java 虚拟机)来解释执行,目的就是将统一的字节码转成操作系统可以识别的二进制码,然后执行。而针对不同的操作系统,都有相应版本的 JVM,所以 Java 就实现了可移植性。
**3)安全性**
Java 适用于网络/分布式环境,为了达到这个目标,在安全方面投入了巨大的精力。使用 Java 可以构建防病毒、防篡改的程序。
从一开始,Java 就设计了很多可以防范攻击的机制,比如说:
- 运行时堆栈溢出,这是蠕虫病毒常用的攻击手段。
- 字节码验证,可以确保代码符合 JVM 规范并防止恶意代码破坏运行时环境。
- 安全的类加载,可以防止不受信任的代码干扰 Java 程序的运行。
- 全面的 API 支持广泛的加密服务,包括数字签名、消息摘要、(对称、非对称)密码、密钥生成器。
- 安全通信,支持 HTTPS、SSL,保护传输的数据完整性和隐私性。
**4)并发性**
Java 在多线程方面做得非常突出,只要操作系统支持,Java 中的线程就可以利用多个处理器,带来了更好的交互响应和实时行为。
“二哥,那 Java 还会继续流行下去吗?”三妹眨了眨她的长睫毛,对我说。
“当然。”我斩钉截铁地回答到。
**大数据领域:**
与 Python 一样,Java 在大数据领域占据着主导地位,很多用于处理大规模数据的框架都是基于 Java 开发的。
- Apache Hadoop,用于在分布式环境中处理大规模数据集。Hadoop 采用了主副架构模式,其中主节点负责控制整个分布式计算栈。Hadoop 在需要处理和分析大规模数据的公司当中很流行。
- Apache Spark,大型的 ETL(数据仓库技术)、预测分析和报表程序经常使用到 Spark。
- Apache Mahout,用于机器学习,比如分类、聚类和推荐。
- JFreechart,用于可视化数据,可以用它制作各种图表,比如饼图、柱状图、线图、散点图、盒状图、直方图等等。
- Deeplearning4j,用于构建各种类型的神经网络,可以与 Spark 集成,运行在 GPU(图形处理器)上。
- Apache Storm,用于处理实时数据流,一个 Storm 节点可以在秒级处理数百万个作业。
**物联网(IoT)领域:**
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/three-02.png)
Oracle 表示,灵活性和流行度是 IoT 程序员选择 Java 的主要原因。Java 提供了大量的 API 库,可以很容易应用到嵌入式应用程序中。相比其他编程语言,比如 C 语言,Java 在切换平台时更加顺畅,不容易出错。
**金融服务领域:**
- 聊天机器人,由于可移植性、可维护性、可视化等诸多方面的因素,Java 成了开发聊天机器人最好的工具。
- 欺诈检测和管理,银行和金融公司使用 AI(人工智能)工具来进行金融欺诈和信用卡欺诈检测,而 Java 常用来开发这些 AI 工具。
- 交易系统,Java 虚拟机提供的动态运行时编译优化在很多情况下比编译型语言(如 C++)具有更好的性能,让交易系统运行得更顺畅。
- 移动钱包,基于 AI 和 Java 算法开发的移动钱包,可以帮助用户在花钱时做出更智能的决策。
**Web 领域:**
Java 技术对 Web 领域的发展注入了强大的动力,主流的 Java Web 开发框架有很多:
- Spring 框架,一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架,渗透了 Java EE 技术的方方面面,绝大部分 Java 应用都可以从 Spring 框架中受益。
- Spring MVC 框架,是一种基于 Java 实现的 MVC(Model-View-Controller)设计模式的请求驱动类型的轻量级 Web 框架。
- MyBatis 框架,一个优秀的数据持久层框架,可在实体类和 SQL 语句之间建立映射关系,是一种半自动化的 ORM(Object Relational Mapping,对象关系映射)实现。
- JavaServer Faces 框架,由 Oracle 开发,能够将表示层与应用程序代码轻松连接,它提供了一个 API 集,用于表示和管理 UI 组件。
总之,Oracle 宣称,Java 正运行在 97% 的企业计算机上——有点厉害的样子。
\ No newline at end of file
20 世纪 90 年代,单片式计算机系统诞生。单片式计算机系统不仅廉价(之前的计算机非常庞大,并且昂贵),而且功能强大,可以大幅度提升消费性电子产品的智能化程度。
Sun 公司为了抢占市场先机,在 1991 年成立了一个由詹姆斯·高斯林(James Gosling)领导的,名为“Green”的项目组,目的是开发一种能够在各种消费性电子产品上运行的程序架构。
项目组首先考虑的是采用 C++ 来编写程序,但 C++ 过于复杂和庞大,再加上消费电子产品所采用的嵌入式处理器芯片的种类繁杂,需要让编写的程序能够跨平台运行并不容易——C++ 在跨平台方面做得并不好。
思前想后,项目组最后决定:在 C++ 的基础上创建一种新的编程语言,既能够剔除 C++ 复杂的指针和内存管理,还能够兼容各种设备。这语言最初的名字叫做 **Greentalk**,文件扩展名为 `.gt`。这个名字叫的比较随意,就因为项目组叫 Green,没什么特殊的寓意。
**Oak** 是“Java”的第二个名字,这次就有点意义了。Oak(橡树)是力量的象征,被美国、法国、德国等许多欧美国家选为国树。橡树长下面这样。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/two-01.png)
1992 年,Oak 的雏形有了,但项目组在向硬件生产商进行商演的时候,并没有获得认可,于是 Oak 就被搁置一旁了。
1994 年,项目组发现 Java 更适合进行 Internet 编程。随后,项目组用 Oak 语言研发了一种能将小程序嵌入到网页中执行的技术——Applet。Applet 不仅能嵌入网页,还能够随同网页在网络上进行传输。
不得不感慨一下,技术的更新迭代是真的快,Applet 拯救了 Oak,并使其蜕变成顶天立地的 Java,但很早之前就被无情地拍死在了沙滩上。是不是很残酷?
1995 年,Oak 被重新命名为“Java”,因为 Oak 被别的公司注册过了。新的名字最好能够表达出技术的本质:dynamic(动态的)、revolutionary(革命性的)、Silk(像丝绸一样柔软的)、Cool(炫酷的)等等。另外,名字一定要容易拼写,念起来也比较有趣。
选来选去,项目组最后选择了“Java”,中文叫“爪哇”。细心的小伙伴可能会发现,Java 这个单词里有一个敏感词,所以有段时间微信(文章专辑名这块)为了禁敏感词,竟然把 Java 都禁了,我当时就只能用爪哇来代替 Java,手动狗头。
“Java”是印度尼西亚爪哇岛的英文名,因生产咖啡而闻名,所以,小伙伴也看到了,Java 这个单词经常和一杯冒着热气的咖啡一起出现。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/two-02.png)
同年,Sun 公司在 SunWorld 大会上正式发布了 Java 1.0 版本,第一次提出了“Write Once, Run anywhere”的口号。《时代》杂志将 Java 评为 1995 年十大最佳产品之一。
1996 年 1 月 23 日,JDK 1.0 发布,Java 语言有了第一个正式版本的运行环境。JDK 1.0 是一个纯解释执行的 Java 虚拟机,代表技术有:Java 虚拟机、AWT(图形化界面)、Applet。
4 月,十个主要的操作系统和计算机供应商宣称将在产品中嵌入 Java 技术。9 月,已有大约 8.3 万网页应用采用了 Java 来制作。5 月底,第一届 JavaOne 大会在旧金山举行,从此,JavaOne 成为全世界数百万 Java 语言开发者的技术盛会。
1997 年 2 月 19 日,JDK 1.1 发布,代表技术有:JAR 文件格式、JDBC、JavaBeans、RMI(远程方法调用)。
1998 年 12 月 4 日,JDK 1.2 发布,这是一个里程碑式的版本。Sun 在这个版本中把 Java 拆分为三个方向:面向桌面开发的 J2SE、面向企业开发的 J2EE,面向移动开发的 J2ME。代表技术有:EJB、Swing。
2000 年 5 月 8 日,JDK 1.3 发布,对 Java 2D 做了大幅修改。
2002 年 2 月 13 日,JDK 1.4 发布,这是 Java 真正走向成熟的一个版本,IBM、富士通等著名公司都有参与。代表技术有:正则表达式、NIO。
2004 年 9 月 30 日,JDK 5 发布,注意 Sun 把“1.x”的命名方式抛弃了。JDK 5 在 Java 语法的易用性上做出了非常大的改进,比如说:自动装箱、泛型、动态注解、枚举、可变参数、foreach 循环。
2006 年 12 月 11 日,JDK 6 发布,J2SE 变成了 Java SE 6,J2EE 变成了 Java EE 6,J2ME 变成了 Java ME 6。JDK 6 恐怕是 Java 历史上使用寿命最长的一个版本了。主要的原因有:代码复杂性的增加、世界经济危机、Oracle 对 Sun 的收购。
JDK 6 的最后一个升级补丁为 Java SE 6 Update 211, 于 2018 年 10 月 18 日发布——12 年的跨度啊!
2009 年 2 月 19 日,JDK 7 发布,但功能是阉割。很多翘首以盼的功能都没有完成,比如说 Lambda 表达式。主要是因为 Sun 公司在商业上陷入了泥沼,已经无力推动 JDK 7 的研发工作。
2009 年 4 月 20 日,Oracle 以 74 亿美元的价格收购了市值曾超过 2000 亿美元的 Sun 公司——太阳终究还是落山了。对于 Java 语言这个孩子来说,可以说是好事,也可以说是坏事。好事是 Oracle 有钱,能够注入资金推动 Java 的发展;坏处就是 Oracle 是后爸,对 Java 肯定没有 Sun 那么亲,走的是极具商业化的道路。
2014 年 3 月 18 日,JDK 8 终于来了,步伐是那么蹒跚,但终究还是来了。带着最强有力的武器——Lambda 表达式而来。虽然 JDK 15 已经发布了,但“新版任你发,我用 Java 8”的梗至今还流传着。
2017 年 9 月 21 日,JDK 9 发布。从此以后,JDK 更新版本的速度令开发者应接不暇,半年一个版本,虽然 Oracle 的目的是好的,为了避免因功能增加而引发的跳票风险,但不得不承认,版本更新的节奏实在是有点过于频繁。
这就导致一个问题,好不容易更新一个版本,用了六个月后,Oracle 不维护了。针对这个问题,Oracle 给出的解决方案挺奇葩的,每六个 JDK 大版本才会被长期支持(Long Term Support,LTS)。
JDK 8 是 LTS 版,2018 年 9 月 25 日发布的 JDK 11 是 LTS 版, 2018 年 3 月 20 日发布的 JDK 10 就可以一笔带过了。按照这个节奏算下去的话,下一个 LTS 版就是 2021 年发布的 JDK 17 了。
JDK 12、JDK 13、JDK 14、JDK 15、JDK 16 都是过渡产品,就好像是试验品一样,不太受开发者待见。
Java 发展到今天已经 20 多年了,作为一个编程语言确实不简单,Java 代表的面向对象思想确实给工程领域带来了革命性的变化,关键是 Java 一直在拥抱变化。
大数据方面,有 Apache Kafka、Apache Samza、Apache Storm、Apache Spark、Apache Flink,除了 Spark 是基于 JVM 的函数语言 Scala 编写的,其余都是 Java 编写的。
Java 在云时代面临着以 Go 语言为主的容器(Docker 等技术)生态圈的挑战,但是,Java 的大型分布式系统越来越多,Java 在云计算与分布式系统中还是扮演着主要角色,并且形成了一个大型的生态圈。
虽然 Java 和 C++,C 一样,都“老”了,被其他语言不断地挑战,但只有强者才有机会接受挑战,对吧?我相信,Java 的未来依然很光明。
\ No newline at end of file
“二哥,之前的文章里提到 JDK 与 JRE,说实在的,这两个概念把我搞得晕乎乎的,你能再给我普及一下吗?”三妹咪了一口麦香可可奶茶后对我说。
“三妹,不要担心,二哥这篇文章一定会让你把它们搞得一清二楚。确实有不少初学的小伙伴对这两个概念很困惑,我当年也困惑了很久。”说完最后这句话,我脸上忍不住泛起了一阵羞涩的红晕。
### 01、JDK
JDK 是 Java Development Kit 的首字母缩写,是提供给 Java 程序员的开发工具包,换句话说,没有 JDK,Java 程序员就无法使用 Java 语言编写 Java 程序。也就是说,JDK 是用于开发 Java 程序的最小环境。
想要成为一名 Java 程序员,首先就需要在电脑上安装 JDK。当然了,新版的 Intellij IDEA(公认最好用的集成开发环境)已经支持直接下载 JDK 了。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-01.png)
并且支持下载不同版本的 JDK,除了 Oracle 的 OpenJDK,还有社区维护版 AdoptOpenJDK,里面包含了目前使用范围最广的 HotSpot 虚拟机。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-02.png)
如果下载比较慢的话,可以直接在 AdoptOpenJDK 官网上下载。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-03.png)
如果还是比较慢的话,通过 Oracle 官网下载吧!
>https://www.oracle.com/java/technologies/javase-jdk11-downloads.html
JDK 安装成功后,就可以编写 Java 代码了,小伙伴们可以参照上一篇文章《[Hello World](https://mp.weixin.qq.com/s/GYDFndO0Q1Nqzcc_Te61gw)》。
JDK 包含了 JRE,同时还包含了编译 Java 源码的编译器 javac,以及其他的一些重要工具:
- keytool:用于操作 keystore 密钥;
- javap:class 类文件的最基础的反编译器;
- jstack:用于打印 Java 线程栈踪迹的工具;
- jconsole:用于监视 Java 程序的工具;
- jhat:用于 Java 堆分析的工具
- jar:用于打包 Java 程序的工具;
- javadoc:用于生成 Java 文档的工具;
### 02、JRE
JRE 是 Java Runtime Environment 的首字母缩写,是提供给 Java 程序运行的最小环境,换句话说,没有 JRE,Java 程序就无法运行。
Java 程序运行的正式环境一般会选择 Linux 服务器,因为更安全、更高效、更稳定。我们只需要在 Linux 服务器上安装 JRE 就可以运行 Java 程序,而不必安装 JDK,因为我们不需要在服务器上编译和调试 Java 源代码。
刚好我有一台闲置的阿里云服务器,这里就给小伙伴们演示一下 JRE 的安装过程。
第一步:使用以下命令列出服务器上可以安装的 Java 环境:
>yum list java*
可以看到有这么一些(只列出 Java 11 的部分——最近一个 LTS 版本):
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-04.png)
其中 JRE 为 java-11-openjdk.x86_64,JDK 为 java-11-openjdk-devel.x86_64。
第二步,使用以下命令安装 JRE:
>yum install java-11-openjdk.x86_64
第三步,使用以下命令测试是否安装成功:
>java -version
如果出现以下结果,则表明安装成功:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-05.png)
由于 JRE 中不包含 javac,所以 `javac -version` 的结果如下所示:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-06.png)
那既然服务器上的 JRE 环境已经 OK 了,那我们就把之前的“Hello World”程序打成 jar 上传过去,让它跑起来。
第一步,Maven clean(对项目清理):
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-07.png)
第二步,Maven package(对项目打包):
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-08.png)
可以在 Run 面板中看到以下信息:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-09.png)
说明项目打包成功了。
第三步,使用 FileZilla 工具将 jar 包上传到服务器指定目录。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-10.png)
第四步,使用 iTerm2 工具连接服务器。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-11.png)
第五步,执行以下命令:
>java -cp TechSister-1.0-SNAPSHOT.jar com.itwanger.five.HelloWorld
可以看到以下结果:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-12.png)
“搞定了,三妹,今天我们就学到这吧。”转动了一下僵硬的脖子后,我对三妹说,“开发环境需要安装 JDK,因为既需要编写源代码,还需要打包和测试;生产环境只需要安装 JRE,因为只需要运行编译打包好的 jar 包即可。”
“好的,二哥,能把你的服务器账号密码给我一下吗,我想上去测试一把。”三妹似乎对未来充满了希望,这正是我想看到的。
“没问题,随便倒腾。”
\ No newline at end of file
---
icon: edit
date: 2022-01-01
category:
- Java核心
tag:
- Java概述
- Java
---
# 什么是 Java?
# 什么是Java?Java发展简史,Java的优势
## 一、什么是 Java?
“二哥,到底什么是 Java?给我说说呗。”
......@@ -83,4 +83,160 @@ public class HelloWorld {
- 实现了热点代码检测和运行时编译,使得 Java 应用能随着运行时间的增长而获得更高的性能;
- 有一套完善的应用程序接口,还有无数来自商业机构和开源社区的第三方类库。
这一切的一切,都让软件开发的效率大大的提高。所以,学习 Java 还是很有“钱”“秃”的。
\ No newline at end of file
这一切的一切,都让软件开发的效率大大的提高。所以,学习 Java 还是很有“钱”“秃”的。
## 二、Java的发展简史
20 世纪 90 年代,单片式计算机系统诞生。单片式计算机系统不仅廉价(之前的计算机非常庞大,并且昂贵),而且功能强大,可以大幅度提升消费性电子产品的智能化程度。
Sun 公司为了抢占市场先机,在 1991 年成立了一个由詹姆斯·高斯林(James Gosling)领导的,名为“Green”的项目组,目的是开发一种能够在各种消费性电子产品上运行的程序架构。
项目组首先考虑的是采用 C++ 来编写程序,但 C++ 过于复杂和庞大,再加上消费电子产品所采用的嵌入式处理器芯片的种类繁杂,需要让编写的程序能够跨平台运行并不容易——C++ 在跨平台方面做得并不好。
思前想后,项目组最后决定:在 C++ 的基础上创建一种新的编程语言,既能够剔除 C++ 复杂的指针和内存管理,还能够兼容各种设备。这语言最初的名字叫做 **Greentalk**,文件扩展名为 `.gt`。这个名字叫的比较随意,就因为项目组叫 Green,没什么特殊的寓意。
**Oak** 是“Java”的第二个名字,这次就有点意义了。Oak(橡树)是力量的象征,被美国、法国、德国等许多欧美国家选为国树。橡树长下面这样。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/two-01.png)
1992 年,Oak 的雏形有了,但项目组在向硬件生产商进行商演的时候,并没有获得认可,于是 Oak 就被搁置一旁了。
1994 年,项目组发现 Java 更适合进行 Internet 编程。随后,项目组用 Oak 语言研发了一种能将小程序嵌入到网页中执行的技术——Applet。Applet 不仅能嵌入网页,还能够随同网页在网络上进行传输。
不得不感慨一下,技术的更新迭代是真的快,Applet 拯救了 Oak,并使其蜕变成顶天立地的 Java,但很早之前就被无情地拍死在了沙滩上。是不是很残酷?
1995 年,Oak 被重新命名为“Java”,因为 Oak 被别的公司注册过了。新的名字最好能够表达出技术的本质:dynamic(动态的)、revolutionary(革命性的)、Silk(像丝绸一样柔软的)、Cool(炫酷的)等等。另外,名字一定要容易拼写,念起来也比较有趣。
选来选去,项目组最后选择了“Java”,中文叫“爪哇”。细心的小伙伴可能会发现,Java 这个单词里有一个敏感词,所以有段时间微信(文章专辑名这块)为了禁敏感词,竟然把 Java 都禁了,我当时就只能用爪哇来代替 Java,手动狗头。
“Java”是印度尼西亚爪哇岛的英文名,因生产咖啡而闻名,所以,小伙伴也看到了,Java 这个单词经常和一杯冒着热气的咖啡一起出现。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/two-02.png)
同年,Sun 公司在 SunWorld 大会上正式发布了 Java 1.0 版本,第一次提出了“Write Once, Run anywhere”的口号。《时代》杂志将 Java 评为 1995 年十大最佳产品之一。
1996 年 1 月 23 日,JDK 1.0 发布,Java 语言有了第一个正式版本的运行环境。JDK 1.0 是一个纯解释执行的 Java 虚拟机,代表技术有:Java 虚拟机、AWT(图形化界面)、Applet。
4 月,十个主要的操作系统和计算机供应商宣称将在产品中嵌入 Java 技术。9 月,已有大约 8.3 万网页应用采用了 Java 来制作。5 月底,第一届 JavaOne 大会在旧金山举行,从此,JavaOne 成为全世界数百万 Java 语言开发者的技术盛会。
1997 年 2 月 19 日,JDK 1.1 发布,代表技术有:JAR 文件格式、JDBC、JavaBeans、RMI(远程方法调用)。
1998 年 12 月 4 日,JDK 1.2 发布,这是一个里程碑式的版本。Sun 在这个版本中把 Java 拆分为三个方向:面向桌面开发的 J2SE、面向企业开发的 J2EE,面向移动开发的 J2ME。代表技术有:EJB、Swing。
2000 年 5 月 8 日,JDK 1.3 发布,对 Java 2D 做了大幅修改。
2002 年 2 月 13 日,JDK 1.4 发布,这是 Java 真正走向成熟的一个版本,IBM、富士通等著名公司都有参与。代表技术有:正则表达式、NIO。
2004 年 9 月 30 日,JDK 5 发布,注意 Sun 把“1.x”的命名方式抛弃了。JDK 5 在 Java 语法的易用性上做出了非常大的改进,比如说:自动装箱、泛型、动态注解、枚举、可变参数、foreach 循环。
2006 年 12 月 11 日,JDK 6 发布,J2SE 变成了 Java SE 6,J2EE 变成了 Java EE 6,J2ME 变成了 Java ME 6。JDK 6 恐怕是 Java 历史上使用寿命最长的一个版本了。主要的原因有:代码复杂性的增加、世界经济危机、Oracle 对 Sun 的收购。
JDK 6 的最后一个升级补丁为 Java SE 6 Update 211, 于 2018 年 10 月 18 日发布——12 年的跨度啊!
2009 年 2 月 19 日,JDK 7 发布,但功能是阉割。很多翘首以盼的功能都没有完成,比如说 Lambda 表达式。主要是因为 Sun 公司在商业上陷入了泥沼,已经无力推动 JDK 7 的研发工作。
2009 年 4 月 20 日,Oracle 以 74 亿美元的价格收购了市值曾超过 2000 亿美元的 Sun 公司——太阳终究还是落山了。对于 Java 语言这个孩子来说,可以说是好事,也可以说是坏事。好事是 Oracle 有钱,能够注入资金推动 Java 的发展;坏处就是 Oracle 是后爸,对 Java 肯定没有 Sun 那么亲,走的是极具商业化的道路。
2014 年 3 月 18 日,JDK 8 终于来了,步伐是那么蹒跚,但终究还是来了。带着最强有力的武器——Lambda 表达式而来。虽然 JDK 15 已经发布了,但“新版任你发,我用 Java 8”的梗至今还流传着。
2017 年 9 月 21 日,JDK 9 发布。从此以后,JDK 更新版本的速度令开发者应接不暇,半年一个版本,虽然 Oracle 的目的是好的,为了避免因功能增加而引发的跳票风险,但不得不承认,版本更新的节奏实在是有点过于频繁。
这就导致一个问题,好不容易更新一个版本,用了六个月后,Oracle 不维护了。针对这个问题,Oracle 给出的解决方案挺奇葩的,每六个 JDK 大版本才会被长期支持(Long Term Support,LTS)。
JDK 8 是 LTS 版,2018 年 9 月 25 日发布的 JDK 11 是 LTS 版, 2018 年 3 月 20 日发布的 JDK 10 就可以一笔带过了。按照这个节奏算下去的话,下一个 LTS 版就是 2021 年发布的 JDK 17 了。
JDK 12、JDK 13、JDK 14、JDK 15、JDK 16 都是过渡产品,就好像是试验品一样,不太受开发者待见。
Java 发展到今天已经 20 多年了,作为一个编程语言确实不简单,Java 代表的面向对象思想确实给工程领域带来了革命性的变化,关键是 Java 一直在拥抱变化。
大数据方面,有 Apache Kafka、Apache Samza、Apache Storm、Apache Spark、Apache Flink,除了 Spark 是基于 JVM 的函数语言 Scala 编写的,其余都是 Java 编写的。
Java 在云时代面临着以 Go 语言为主的容器(Docker 等技术)生态圈的挑战,但是,Java 的大型分布式系统越来越多,Java 在云计算与分布式系统中还是扮演着主要角色,并且形成了一个大型的生态圈。
虽然 Java 和 C++,C 一样,都“老”了,被其他语言不断地挑战,但只有强者才有机会接受挑战,对吧?我相信,Java 的未来依然很光明。
## 三、Java的优势
尽管 Java 已经 25 岁了,但仍然“宝刀未老”。在 Stack Overflow 2019 年流行编程语言调查报告中,Java 位居第 5 位,有 41% 的受调开发者认为 Java 仍然是一门受欢迎的编程语言。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/three-01.png)
很多大型的互联网公司都在使用 Java,国内最有名的当属阿里巴巴,国外最有名的当属谷歌。那为什么 Java 如此流行呢?
**1)简单性**
Java 为开发者提供了简单易用的用户体验,与其他面向对象编程语言相比,Java 的设计和生态库具有巨大的优势。Java 剔除了 C++ 中很少使用、难以理解、易混淆的特别,比如说指针运算、操作符重载,内存管理等。
Java 可以做到堆栈分配、垃圾回收和自动内存管理,在一定程度上为开发者减轻了入门的难度。
**2)可移植性**
如果 Java 直接编译成操作系统能识的二进制码,可能一个标识在 Windows 操作系统下是1100,而 Linux 下是 1001,这样的话,在 Windows 操作系统下可以运行的程序到了 Linux 环境下就无法运行。
为了解决这个问题,Java 先编译生成字节码,再由 JVM(Java 虚拟机)来解释执行,目的就是将统一的字节码转成操作系统可以识别的二进制码,然后执行。而针对不同的操作系统,都有相应版本的 JVM,所以 Java 就实现了可移植性。
**3)安全性**
Java 适用于网络/分布式环境,为了达到这个目标,在安全方面投入了巨大的精力。使用 Java 可以构建防病毒、防篡改的程序。
从一开始,Java 就设计了很多可以防范攻击的机制,比如说:
- 运行时堆栈溢出,这是蠕虫病毒常用的攻击手段。
- 字节码验证,可以确保代码符合 JVM 规范并防止恶意代码破坏运行时环境。
- 安全的类加载,可以防止不受信任的代码干扰 Java 程序的运行。
- 全面的 API 支持广泛的加密服务,包括数字签名、消息摘要、(对称、非对称)密码、密钥生成器。
- 安全通信,支持 HTTPS、SSL,保护传输的数据完整性和隐私性。
**4)并发性**
Java 在多线程方面做得非常突出,只要操作系统支持,Java 中的线程就可以利用多个处理器,带来了更好的交互响应和实时行为。
“二哥,那 Java 还会继续流行下去吗?”三妹眨了眨她的长睫毛,对我说。
“当然。”我斩钉截铁地回答到。
**大数据领域:**
与 Python 一样,Java 在大数据领域占据着主导地位,很多用于处理大规模数据的框架都是基于 Java 开发的。
- Apache Hadoop,用于在分布式环境中处理大规模数据集。Hadoop 采用了主副架构模式,其中主节点负责控制整个分布式计算栈。Hadoop 在需要处理和分析大规模数据的公司当中很流行。
- Apache Spark,大型的 ETL(数据仓库技术)、预测分析和报表程序经常使用到 Spark。
- Apache Mahout,用于机器学习,比如分类、聚类和推荐。
- JFreechart,用于可视化数据,可以用它制作各种图表,比如饼图、柱状图、线图、散点图、盒状图、直方图等等。
- Deeplearning4j,用于构建各种类型的神经网络,可以与 Spark 集成,运行在 GPU(图形处理器)上。
- Apache Storm,用于处理实时数据流,一个 Storm 节点可以在秒级处理数百万个作业。
**物联网(IoT)领域:**
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/three-02.png)
Oracle 表示,灵活性和流行度是 IoT 程序员选择 Java 的主要原因。Java 提供了大量的 API 库,可以很容易应用到嵌入式应用程序中。相比其他编程语言,比如 C 语言,Java 在切换平台时更加顺畅,不容易出错。
**金融服务领域:**
- 聊天机器人,由于可移植性、可维护性、可视化等诸多方面的因素,Java 成了开发聊天机器人最好的工具。
- 欺诈检测和管理,银行和金融公司使用 AI(人工智能)工具来进行金融欺诈和信用卡欺诈检测,而 Java 常用来开发这些 AI 工具。
- 交易系统,Java 虚拟机提供的动态运行时编译优化在很多情况下比编译型语言(如 C++)具有更好的性能,让交易系统运行得更顺畅。
- 移动钱包,基于 AI 和 Java 算法开发的移动钱包,可以帮助用户在花钱时做出更智能的决策。
**Web 领域:**
Java 技术对 Web 领域的发展注入了强大的动力,主流的 Java Web 开发框架有很多:
- Spring 框架,一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架,渗透了 Java EE 技术的方方面面,绝大部分 Java 应用都可以从 Spring 框架中受益。
- Spring MVC 框架,是一种基于 Java 实现的 MVC(Model-View-Controller)设计模式的请求驱动类型的轻量级 Web 框架。
- MyBatis 框架,一个优秀的数据持久层框架,可在实体类和 SQL 语句之间建立映射关系,是一种半自动化的 ORM(Object Relational Mapping,对象关系映射)实现。
- JavaServer Faces 框架,由 Oracle 开发,能够将表示层与应用程序代码轻松连接,它提供了一个 API 集,用于表示和管理 UI 组件。
总之,Oracle 宣称,Java 正运行在 97% 的企业计算机上——有点厉害的样子。
\ No newline at end of file
---
category:
- Java核心
tag:
- Java
---
# 深入了解Java字符串常量池
“三妹,今天我们来学习一下字符串常量池吧,这是字符串中非常关键的一个知识点。”我话音未落,青岛路小学那边传来了嘹亮的歌声就钻进了我的耳朵,“唱 ~ 山 ~ 歌 ~”
......
---
category:
- Java核心
tag:
- Java
---
# Java判断两个字符串是否相等?
“哥,如何比较两个字符串相等啊?”三妹问。
......
---
category:
- Java核心
tag:
- Java
---
# 为什么String是不可变的?
我正坐在沙发上津津有味地读刘欣大佬的《码农翻身》——Java 帝国这一章,门铃响了。起身打开门一看,是三妹,她从学校回来了。
......
---
category:
- Java核心
tag:
- Java
---
# 深入解析 String#intern
“哥,你发给我的那篇文章我看了,结果直接把我给看得不想学 Java 了!”三妹气冲冲地说。
......
---
category:
- Java核心
tag:
- Java
---
# Java字符串拼接的几种方式
“哥,你让我看的《Java 开发手册》上有这么一段内容:循环体内,拼接字符串最好使用 StringBuilder 的 `append()` 方法,而不是 + 号操作符。这是为什么呀?”三妹疑惑地问。
......
---
category:
- Java核心
tag:
- Java
---
# 如何在Java中优雅地分割String字符串?
“哥,我感觉字符串拆分没什么可讲的呀,直接上 String 类的 `split()` 方法不就可以了!”三妹毫不客气地说。
......@@ -209,8 +216,3 @@ if (cmower.contains(",")) {
“嗯,我把今天的内容温习下,二哥,你休息会。”三妹说。
----
**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享!
<img src="https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png" alt="图片没显示的话,可以微信搜索「沉默王二」关注" style="zoom:50%;" />
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册