)
+
+本文我们就来围绕这个问题来分析一下为什么JDK自身提供的构建线程池的方式并不建议使用?到底应该如何创建一个线程池呢?
+
+### Executors
+
+Executors 是一个Java中的工具类。提供工厂方法来创建不同类型的线程池。
+
+![][2]
+
+从上图中也可以看出,Executors的创建线程池的方法,创建出来的线程池都实现了ExecutorService接口。常用方法有以下几个:
+
+`newFiexedThreadPool(int Threads)`:创建固定数目线程的线程池。
+
+`newCachedThreadPool()`:创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
+
+`newSingleThreadExecutor()`创建一个单线程化的Executor。
+
+`newScheduledThreadPool(int corePoolSize)`创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
+
+类看起来功能还是比较强大的,又用到了工厂模式、又有比较强的扩展性,重要的是用起来还比较方便,如:
+
+ExecutorService executor = Executors.newFixedThreadPool(nThreads) ;
+
+
+即可创建一个固定大小的线程池。
+
+但是为什么我说不建议大家使用这个类来创建线程池呢?
+
+我提到的是『不建议』,但是在阿里巴巴Java开发手册中也明确指出,而且用的词是『不允许』使用Executors创建线程池。
+

+
+### Executors存在什么问题
+
+在阿里巴巴Java开发手册中提到,使用Executors创建线程池可能会导致OOM(OutOfMemory ,内存溢出),但是并没有说明为什么,那么接下来我们就来看一下到底为什么不允许使用Executors?
+
+我们先来一个简单的例子,模拟一下使用Executors导致OOM的情况。
+
+/**
+ * @author Hollis
+ */
+public class ExecutorsDemo {
+ private static ExecutorService executor = Executors.newFixedThreadPool(15);
+ public static void main(String[] args) {
+ for (int i = 0; i < Integer.MAX_VALUE; i++) {
+ executor.execute(new SubThread());
+ }
+ }
+}
+
+class SubThread implements Runnable {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ //do nothing
+ }
+ }
+}
+
+
+通过指定JVM参数:`-Xmx8m -Xms8m` 运行以上代码,会抛出OOM:
+
+Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
+ at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
+ at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
+ at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)
+
+
+以上代码指出,`ExecutorsDemo.java`的第16行,就是代码中的`executor.execute(new SubThread());`。
+
+### Executors为什么存在缺陷
+
+通过上面的例子,我们知道了`Executors`创建的线程池存在OOM的风险,那么到底是什么原因导致的呢?我们需要深入`Executors`的源码来分析一下。
+
+其实,在上面的报错信息中,我们是可以看出蛛丝马迹的,在以上的代码中其实已经说了,真正的导致OOM的其实是`LinkedBlockingQueue.offer`方法。
+
+Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
+ at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
+ at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
+ at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)
+
+
+如果读者翻看代码的话,也可以发现,其实底层确实是通过`LinkedBlockingQueue`实现的:
+
+public static ExecutorService newFixedThreadPool(int nThreads) {
+ return new ThreadPoolExecutor(nThreads, nThreads,
+ 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<Runnable>());
+
+
+如果读者对Java中的阻塞队列有所了解的话,看到这里或许就能够明白原因了。
+
+Java中的`BlockingQueue`主要有两种实现,分别是`ArrayBlockingQueue` 和 `LinkedBlockingQueue`。
+
+`ArrayBlockingQueue`是一个用数组实现的有界阻塞队列,必须设置容量。
+
+`LinkedBlockingQueue`是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为`Integer.MAX_VALUE`。
+
+这里的问题就出在:**不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。**也就是说,如果我们不设置`LinkedBlockingQueue`的容量的话,其默认容量将会是`Integer.MAX_VALUE`。
+
+而`newFixedThreadPool`中创建`LinkedBlockingQueue`时,并未指定容量。此时,`LinkedBlockingQueue`就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。
+
+上面提到的问题主要体现在`newFixedThreadPool`和`newSingleThreadExecutor`两个工厂方法上,并不是说`newCachedThreadPool`和`newScheduledThreadPool`这两个方法就安全了,这两种方式创建的最大线程数可能是`Integer.MAX_VALUE`,而创建这么多线程,必然就有可能导致OOM。
+
+### 创建线程池的正确姿势
+
+避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用`ThreadPoolExecutor`的构造函数来自己创建线程池。在创建的同时,给`BlockQueue`指定容量就可以了。
+
+private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
+ 60L, TimeUnit.SECONDS,
+ new ArrayBlockingQueue(10));
+
+
+这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出`java.util.concurrent.RejectedExecutionException`,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。
+
+除了自己定义`ThreadPoolExecutor`外。还有其他方法。这个时候第一时间就应该想到开源类库,如apache和guava等。
+
+作者推荐使用guava提供的ThreadFactoryBuilder来创建线程池。
+
+public class ExecutorsDemo {
+
+ private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
+ .setNameFormat("demo-pool-%d").build();
+
+ private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
+ 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
+
+ public static void main(String[] args) {
+
+ for (int i = 0; i < Integer.MAX_VALUE; i++) {
+ pool.execute(new SubThread());
+ }
+ }
+}
+
+
+通过上述方式创建线程时,不仅可以避免OOM的问题,还可以自定义线程名称,更加方便的出错的时候溯源。
+
+思考题,文中作者说:发生异常(Exception)要比发生错误(Error)好,为什么这么说?
+
+文中提到的《阿里巴巴Java开发手册》,请关注公众号Hollis,回复:手册。即可获得完整版PDF。
+
+ [1]: https://mp.weixin.qq.com/s/-89-CcDnSLBYy3THmcLEdQ
+ [2]: http://www.hollischuang.com/wp-content/uploads/2018/10/15406248096737.jpg
\ No newline at end of file
diff --git a/docs/basics/java-basic/enum-class.md b/docs/basics/java-basic/enum-class.md
index 58ad1bdb..159fe15a 100644
--- a/docs/basics/java-basic/enum-class.md
+++ b/docs/basics/java-basic/enum-class.md
@@ -11,4 +11,4 @@ Java中定义枚举是使用enum关键字的,但是Java中其实还有一个ja
这个类我们在日常开发中不会用到,但是其实我们使用enum定义的枚举,其实现方式就是通过继承Enum类实现的。
-当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
\ No newline at end of file
+当我们使用enum来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
\ No newline at end of file
diff --git a/docs/basics/java-basic/enum-singleton.md b/docs/basics/java-basic/enum-singleton.md
index 089bd57a..05164b23 100644
--- a/docs/basics/java-basic/enum-singleton.md
+++ b/docs/basics/java-basic/enum-singleton.md
@@ -39,14 +39,14 @@
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
- if (singleton == null) {
- synchronized (Singleton.class) {
if (singleton == null) {
- singleton = new Singleton();
+ synchronized (Singleton.class) {
+ if (singleton == null) {
+ singleton = new Singleton();
+ }
+ }
}
- }
- }
- return singleton;
+ return singleton;
}
}
diff --git a/docs/basics/java-basic/final-string.md b/docs/basics/java-basic/final-string.md
index 920b8e0c..a2e2e05b 100644
--- a/docs/basics/java-basic/final-string.md
+++ b/docs/basics/java-basic/final-string.md
@@ -23,7 +23,7 @@ s = s.concat("ef");
所以,一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。而且,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
-如果我们想要一个可秀改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。
+如果我们想要一个可修改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。
### 为什么String要设计成不可变
@@ -70,7 +70,7 @@ String s2 = s;
当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。
-但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全可信了。这样整个系统就没有安全性可言了。
+但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。
#### 线程安全
diff --git a/docs/basics/java-basic/hash-in-hashmap.md b/docs/basics/java-basic/hash-in-hashmap.md
index ddb52259..4cc4f7e6 100644
--- a/docs/basics/java-basic/hash-in-hashmap.md
+++ b/docs/basics/java-basic/hash-in-hashmap.md
@@ -79,7 +79,7 @@
}
-前面我说过,`indexFor`方法其实主要是将hash生成的整型转换成链表数组中的下标。那么`return h & (length-1);`是什么意思呢?其实,他就是取模。Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。**位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。**
+前面我说过,`indexFor`方法其实主要是将hash生成的整型转换成链表数组中的下标。那么`return h & (length-1);`是什么意思呢?其实,他就是取模。Java之所以使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。**位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。**
那么,为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下:
diff --git a/docs/basics/java-basic/hashmap-default-capacity.md b/docs/basics/java-basic/hashmap-default-capacity.md
index cb561673..423d843f 100644
--- a/docs/basics/java-basic/hashmap-default-capacity.md
+++ b/docs/basics/java-basic/hashmap-default-capacity.md
@@ -194,7 +194,7 @@ Step 1 怎么理解呢?其实是对一个二进制数依次向右移位,然
### 扩容
-除了初始化的时候回指定HashMap的容量,在进行扩容的时候,其容量也可能会改变。
+除了初始化的时候会指定HashMap的容量,在进行扩容的时候,其容量也可能会改变。
HashMap有扩容机制,就是当达到扩容条件时会进行扩容。HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。
diff --git a/docs/basics/java-basic/length-of-string.md b/docs/basics/java-basic/length-of-string.md
index 7c3fcceb..6b9d37a3 100644
--- a/docs/basics/java-basic/length-of-string.md
+++ b/docs/basics/java-basic/length-of-string.md
@@ -134,4 +134,4 @@ int 是一个 32 位变量类型,取正数部分来算的话,他们最长可
在运行期,长度不能超过Int的范围,否则会抛异常。
-最后,这个知识点 ,我录制了视频(https://www.bilibili.com/video/BV1uK4y1t7H1/),其中有关于如何进行实验测试、如何查阅Java规范以及如何对javac进行deubg的技巧。欢迎进一步学习。
+最后,这个知识点 ,我录制了视频([点击跳转](https://www.bilibili.com/video/BV1uK4y1t7H1/)) ,其中有关于如何进行实验测试、如何查阅Java规范以及如何对javac进行deubg的技巧。欢迎进一步学习。
diff --git a/docs/basics/java-basic/stop-create-bigdecimal-with-double.md b/docs/basics/java-basic/stop-create-bigdecimal-with-double.md
new file mode 100644
index 00000000..ee5eed79
--- /dev/null
+++ b/docs/basics/java-basic/stop-create-bigdecimal-with-double.md
@@ -0,0 +1,163 @@
+很多人都知道,在进行金额表示、金额计算等场景,不能使用double、float等类型,而是要使用对精度支持的更好的BigDecimal。
+
+所以,很多支付、电商、金融等业务中,BigDecimal的使用非常频繁。**但是,如果误以为只要使用BigDecimal表示数字,结果就一定精确,那就大错特错了!**
+
+在之前的一篇文章中,我们介绍过,使用BigDecimal的equals方法并不能验证两个数是否真的相等([为什么阿里巴巴禁止使用BigDecimal的equals方法做等值比较?][1])。
+
+除了这个情况,BigDecimal的使用的第一步就是创建一个BigDecimal对象,如果这一步都有问题,那么后面怎么算都是错的!
+
+那到底应该如何正确的创建一个BigDecimal?
+
+**关于这个问题,我Review过很多代码,也面试过很多一线开发,很多人都掉进坑里过。这是一个很容易被忽略,但是又影响重大的问题。**
+
+关于这个问题,在《阿里巴巴Java开发手册》中有一条建议,或者说是要求:
+
+![][2]
+
+这是一条【强制】建议,那么,这背后的原理是什么呢?
+
+想要搞清楚这个问题,主要需要弄清楚以下几个问题:
+
+1、为什么说double不精确? 2、BigDecimal是如何保证精确的?
+
+在知道这两个问题的答案之后,我们也就大概知道为什么不能使用BigDecimal(double)来创建一个BigDecimal了。
+
+### double为什么不精确
+
+首先,**计算机是只认识二进制的**,即0和1,这个大家一定都知道。
+
+那么,所有数字,包括整数和小数,想要在计算机中存储和展示,都需要转成二进制。
+
+**十进制整数转成二进制很简单,通常采用"除2取余,逆序排列"即可,如10的二进制为1010。**
+
+但是,小数的二进制如何表示呢?
+
+十进制小数转成二进制,一般采用"乘2取整,顺序排列"方法,如0.625转成二进制的表示为0.101。
+
+但是,并不是所有小数都能转成二进制,如0.1就不能直接用二进制表示,他的二进制是0.000110011001100… 这是一个无限循环小数。
+
+**所以,计算机是没办法用二进制精确的表示0.1的。也就是说,在计算机中,很多小数没办法精确的使用二进制表示出来。**
+
+那么,这个问题总要解决吧。那么,**人们想出了一种采用一定的精度,使用近似值表示一个小数的办法**。这就是IEEE 754(IEEE二进制浮点数算术标准)规范的主要思想。
+
+IEEE 754规定了多种表示浮点数值的方式,其中最常用的就是32位单精度浮点数和64位双精度浮点数。
+
+在Java中,使用float和double分别用来表示单精度浮点数和双精度浮点数。
+
+所谓精度不同,可以简单的理解为保留有效位数不同。采用保留有效位数的方式近似的表示小数。
+
+所以,大家也就知道为什么**double表示的小数不精确**了。
+
+接下来,再回到BigDecimal的介绍,我们接下来看看是如何表示一个数的,他如何保证精确呢?
+
+### BigDecimal如何精确计数?
+
+如果大家看过BigDecimal的源码,其实可以发现,**实际上一个BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。**
+
+在BigDecimal中,标度是通过scale字段来表示的。
+
+而无标度值的表示比较复杂。当unscaled value超过阈值(默认为Long.MAX_VALUE)时采用intVal字段存储unscaled value,intCompact字段存储Long.MIN_VALUE,否则对unscaled value进行压缩存储到long型的intCompact字段用于后续计算,intVal为空。
+
+涉及到的字段就是这几个:
+
+ public class BigDecimal extends Number implements Comparable {
+ private final BigInteger intVal;
+ private final int scale;
+ private final transient long intCompact;
+ }
+
+
+关于无标度值的压缩机制大家了解即可,不是本文的重点,大家只需要知道BigDecimal主要是通过一个无标度值和标度来表示的就行了。
+
+**那么标度到底是什么呢?**
+
+除了scale这个字段,在BigDecimal中还提供了scale()方法,用来返回这个BigDecimal的标度。
+
+ /**
+ * Returns the scale of this {@code BigDecimal}. If zero
+ * or positive, the scale is the number of digits to the right of
+ * the decimal point. If negative, the unscaled value of the
+ * number is multiplied by ten to the power of the negation of the
+ * scale. For example, a scale of {@code -3} means the unscaled
+ * value is multiplied by 1000.
+ *
+ * @return the scale of this {@code BigDecimal}.
+ */
+ public int scale() {
+ return scale;
+ }
+
+
+那么,scale到底表示的是什么,其实上面的注释已经说的很清楚了:
+
+> 如果scale为零或正值,则该值表示这个数字小数点右侧的位数。如果scale为负数,则该数字的真实值需要乘以10的该负数的绝对值的幂。例如,scale为-3,则这个数需要乘1000,即在末尾有3个0。
+
+如123.123,那么如果使用BigDecimal表示,那么他的无标度值为123123,他的标度为3。
+
+**而二进制无法表示的0.1,使用BigDecimal就可以表示了,及通过无标度值1和标度1来表示。**
+
+我们都知道,想要创建一个对象,需要使用该类的构造方法,在BigDecimal中一共有以下4个构造方法:
+
+ BigDecimal(int)
+ BigDecimal(double)
+ BigDecimal(long)
+ BigDecimal(String)
+
+
+以上四个方法,创建出来的的BigDecimal的标度(scale)是不同的。
+
+其中 BigDecimal(int)和BigDecimal(long) 比较简单,因为都是整数,所以他们的标度都是0。
+
+而BigDecimal(double) 和BigDecimal(String)的标度就有很多学问了。
+
+### BigDecimal(double)有什么问题
+
+BigDecimal中提供了一个通过double创建BigDecimal的方法——BigDecimal(double) ,但是,同时也给我们留了一个坑!
+
+因为我们知道,double表示的小数是不精确的,如0.1这个数字,double只能表示他的近似值。
+
+所以,**当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是正好等于0.1的。**
+
+而是0.1000000000000000055511151231257827021181583404541015625。这是因为doule自身表示的只是一个近似值。
+
+![][3]
+
+**所以,如果我们在代码中,使用BigDecimal(double) 来创建一个BigDecimal的话,那么是损失了精度的,这是极其严重的。**
+
+### 使用BigDecimal(String)创建
+
+那么,该如何创建一个精确的BigDecimal来表示小数呢,答案是使用String创建。
+
+而对于BigDecimal(String) ,当我们使用new BigDecimal("0.1")创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。
+
+那么他的标度也就是1。
+
+但是需要注意的是,new BigDecimal("0.10000")和new BigDecimal("0.1")这两个数的标度分别是5和1,如果使用BigDecimal的equals方法比较,得到的结果是false,具体原因和解决办法参考[为什么阿里巴巴禁止使用BigDecimal的equals方法做等值比较?][1]
+
+那么,想要创建一个能精确的表示0.1的BigDecimal,请使用以下两种方式:
+
+ BigDecimal recommend1 = new BigDecimal("0.1");
+ BigDecimal recommend2 = BigDecimal.valueOf(0.1);
+
+
+这里,留一个思考题,BigDecimal.valueOf()是调用Double.toString方法实现的,那么,既然double都是不精确的,BigDecimal.valueOf(0.1)怎么保证精确呢?
+
+### 总结
+
+因为计算机采用二进制处理数据,但是很多小数,如0.1的二进制是一个无线循环小数,而这种数字在计算机中是无法精确表示的。
+
+所以,人们采用了一种通过近似值的方式在计算机中表示,于是就有了单精度浮点数和双精度浮点数等。
+
+所以,作为单精度浮点数的float和双精度浮点数的double,在表示小数的时候只是近似值,并不是真实值。
+
+所以,当使用BigDecimal(Double)创建一个的时候,得到的BigDecimal是损失了精度的。
+
+而使用一个损失了精度的数字进行计算,得到的结果也是不精确的。
+
+想要避免这个问题,可以通过BigDecimal(String)的方式创建BigDecimal,这样的情况下,0.1就会被精确的表示出来。
+
+其表现形式是一个无标度数值1,和一个标度1的组合。
+
+ [1]: https://mp.weixin.qq.com/s/iiZW9xr1Xb2JIaRFnWLZUg
+ [2]: https://www.hollischuang.com/wp-content/uploads/2021/01/16119907257353.jpg
+ [3]: https://www.hollischuang.com/wp-content/uploads/2021/01/16119945021181.jpg
\ No newline at end of file
diff --git a/docs/basics/java-basic/syntactic-sugar.md b/docs/basics/java-basic/syntactic-sugar.md
index 8c085333..fb2a1471 100644
--- a/docs/basics/java-basic/syntactic-sugar.md
+++ b/docs/basics/java-basic/syntactic-sugar.md
@@ -268,7 +268,7 @@ Java SE5提供了一种新的类型-Java的枚举类型,关键字`enum`可以
}
-通过反编译后代码我们可以看到,`public final class T extends Enum`,说明,该类是继承了`Enum`类的,同时`final`关键字告诉我们,这个类也是不能被继承的。**当我们使用`enmu`来定义一个枚举类型的时候,编译器会自动帮我们创建一个`final`类型的类继承`Enum`类,所以枚举类型不能被继承。**
+通过反编译后代码我们可以看到,`public final class T extends Enum`,说明,该类是继承了`Enum`类的,同时`final`关键字告诉我们,这个类也是不能被继承的。**当我们使用`enum`来定义一个枚举类型的时候,编译器会自动帮我们创建一个`final`类型的类继承`Enum`类,所以枚举类型不能被继承。**
### 糖块六 、 内部类
diff --git a/docs/basics/object-oriented/java-pass-by.md b/docs/basics/object-oriented/java-pass-by.md
index c05138bc..7d2abf93 100644
--- a/docs/basics/object-oriented/java-pass-by.md
+++ b/docs/basics/object-oriented/java-pass-by.md
@@ -56,7 +56,7 @@
* 传值调用(值传递)
* 在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。因为形式参数拿到的只是一个"局部拷贝",所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。
-* 传引用调用(应用传递)
+* 传引用调用(引用传递)
* 在传引用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。因为传递的是引用,所以,如果在被调函数中改变了形式参数的值,改变对于调用者来说是可见的。
* 传共享对象调用(共享对象传递)
* 传共享对象调用中,先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。因为参数的地址都指向同一个对象,所以我们称也之为"传共享对象",所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。
diff --git a/docs/basics/object-oriented/platform-independent.md b/docs/basics/object-oriented/platform-independent.md
index 880d0a44..dd2f0790 100644
--- a/docs/basics/object-oriented/platform-independent.md
+++ b/docs/basics/object-oriented/platform-independent.md
@@ -118,5 +118,5 @@ Java的平台无关性是建立在Java虚拟机的平台有关性基础之上的
[5]: https://www.hollischuang.com/wp-content/uploads/2019/03/15539291533175.jpg
[6]: https://www.hollischuang.com/wp-content/uploads/2019/03/15539297082025.jpg
[7]: https://www.hollischuang.com/wp-content/uploads/2019/03/15539303829914.jpg
- [8]: http://www.hollischuang.com/wp-content/uploads/2019/03/Jietu20200614-165222.jpg
+ [8]: https://www.hollischuang.com/wp-content/uploads/2021/06/Jietu20210627-141259-2.jpg
[9]: https://www.hollischuang.com/archives/2938
diff --git a/docs/basics/object-oriented/why-pass-by-reference.md b/docs/basics/object-oriented/why-pass-by-reference.md
index 81d37c9d..f76bb96f 100644
--- a/docs/basics/object-oriented/why-pass-by-reference.md
+++ b/docs/basics/object-oriented/why-pass-by-reference.md
@@ -87,6 +87,7 @@
public void pass(User user) {
user = new User();
user.setName("hollischuang");
+ user.setGender("Male");
System.out.println("print in pass , user is " + user);
}
@@ -136,4 +137,4 @@ OK,以上就是本文的全部内容,不知道本文是否帮助你解开了
[3]: https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html
[4]: https://en.wikipedia.org/wiki/Evaluation_strategy
[5]: https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value
- [6]: https://blog.penjee.com/passing-by-value-vs-by-reference-java-graphical/
\ No newline at end of file
+ [6]: https://blog.penjee.com/passing-by-value-vs-by-reference-java-graphical/
diff --git a/docs/contact/book.jpeg b/docs/contact/book.jpeg
new file mode 100644
index 00000000..73a855ec
Binary files /dev/null and b/docs/contact/book.jpeg differ
diff --git a/docs/menu.md b/docs/menu.md
index f034f328..6f69aef5 100644
--- a/docs/menu.md
+++ b/docs/menu.md
@@ -2,9 +2,13 @@
  
+成神之路系列丛书的第一本《深入理解Java核心技术(基础篇)》已经正式出版了,这本书囊括了中基础篇的几乎全部内容,欢迎大家购买品鉴。
+
+
| 主要版本 | 更新时间 | 备注 |
| ---- | ---------- | -------------- |
+| v4.0 | 2022-05-20 | 知识体系完善,知识点补充|
| v3.0 | 2020-03-31 | 知识体系完善,在v2.0的基础上,新增20%左右的知识点
调整部分知识的顺序及结构,方便阅读和理解
通过GitHub Page搭建,便于阅读|
| v2.0 | 2019-02-19 | 结构调整,更适合从入门到精通;
进一步完善知识体系;
新技术补充;|
| v1.1 | 2018-03-12 | 增加新技术知识、完善知识体系 |
@@ -319,7 +323,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* [什么是泛型](/basics/java-basic/generics.md)
- * [类型擦除](/basics/java-basic/type-erasue.md)
+ * [类型擦除](/basics/java-basic/type-erasure.md)
* [泛型带来的问题](/basics/java-basic/generics-problem.md)
@@ -457,7 +461,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* [为什么不能使用BigDecimal的equals比较大小](/basics/java-basic/stop-using-equlas-in-bigdecimal.md)
- * 为什么不能直接使用double创建一个BigDecimal
+ * [为什么不能直接使用double创建一个BigDecimal](/basics/java-basic/stop-create-bigdecimal-with-double.md)
* Java 8
@@ -535,11 +539,11 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 线程池原理
- * 为什么不允许使用Executors创建线程池
+ * [为什么不允许使用Executors创建线程池](/basics/concurrent-coding/why-not-executors.md)
* 线程安全
- * 什么是线程安全
+ * [什么是线程安全](/basics/concurrent-coding/thread-safe.md)
* 多级缓存和一致性问题
diff --git a/pics/book.jpeg b/pics/book.jpeg
new file mode 100644
index 00000000..73a855ec
Binary files /dev/null and b/pics/book.jpeg differ