提交 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
---
# 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
---
# Java IO学习整理
“老王,Java IO 也太上头了吧?”新兵蛋子小二向头顶很凉快的老王抱怨道,“你瞧,我就按照传输方式对 IO 进行了一个简单的分类,就能搞出来这么多的玩意!”
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
---
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 变量在以后的日子里经常用,能不能提前给我透露透露?”三妹咪了一口麦香可可奶茶后对我说。
......
此差异已折叠。
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
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
---
category:
- Java核心
tag:
- Java
---
# 深入了解Java字符串常量池
“三妹,今天我们来学习一下字符串常量池吧,这是字符串中非常关键的一个知识点。”我话音未落,青岛路小学那边传来了嘹亮的歌声就钻进了我的耳朵,“唱 ~ 山 ~ 歌 ~”
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册