)
+
+本文我们就来围绕这个问题来分析一下为什么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/CET-UTC-GMT-CST.md b/docs/basics/java-basic/CET-UTC-GMT-CST.md
index 37e0402b..86872a67 100644
--- a/docs/basics/java-basic/CET-UTC-GMT-CST.md
+++ b/docs/basics/java-basic/CET-UTC-GMT-CST.md
@@ -16,5 +16,7 @@
### 关系
CET=UTC/GMT + 1小时
+
CST=UTC/GMT +8 小时
-CST=CET+9
\ No newline at end of file
+
+CST=CET + 7 小时
diff --git a/docs/basics/java-basic/Wildcard-Character.md b/docs/basics/java-basic/Wildcard-Character.md
index b9e2a1b0..3c173ac0 100644
--- a/docs/basics/java-basic/Wildcard-Character.md
+++ b/docs/basics/java-basic/Wildcard-Character.md
@@ -1,10 +1,10 @@
`限定通配符`对类型进⾏限制, 泛型中有两种限定通配符:
-表示类型的上界,格式为:<? extends T>,即类型必须为T类型或者T子类
-表示类型的下界,格式为:<? super T>,即类型必须为T类型或者T的父类
+表示类型的上界,格式为:`<? extends T>`,即类型必须为T类型或者T子类
+表示类型的下界,格式为:`<? super T>`,即类型必须为T类型或者T的父类
泛型类型必须⽤限定内的类型来进⾏初始化,否则会导致编译错误。
-`⾮限定通配符`表⽰可以⽤任意泛型类型来替代,类型为
\ No newline at end of file
+`⾮限定通配符`表⽰可以⽤任意泛型类型来替代,类型为``
\ No newline at end of file
diff --git a/docs/basics/java-basic/annotation-in-java.md b/docs/basics/java-basic/annotation-in-java.md
index 5863debc..1dc1eb04 100644
--- a/docs/basics/java-basic/annotation-in-java.md
+++ b/docs/basics/java-basic/annotation-in-java.md
@@ -2,11 +2,11 @@
@Override 表示当前方法覆盖了父类的方法
-@Deprecation 表示方法已经过时,方法上有横线,使用时会有警告。
+@Deprecated 表示方法已经过时,方法上有横线,使用时会有警告。
@SuppressWarnings 表示关闭一些警告信息(通知java编译器忽略特定的编译警告)
-SafeVarargs (jdk1.7更新) 表示:专门为抑制“堆污染”警告提供的。
+@SafeVarargs (jdk1.7更新) 表示:专门为抑制“堆污染”警告提供的。
@FunctionalInterface (jdk1.8更新) 表示:用来指定某个接口必须是函数式接口,否则就会编译出错。
diff --git a/docs/basics/java-basic/annotation-in-spring.md b/docs/basics/java-basic/annotation-in-spring.md
index 4d93fd1c..b069d257 100644
--- a/docs/basics/java-basic/annotation-in-spring.md
+++ b/docs/basics/java-basic/annotation-in-spring.md
@@ -26,10 +26,6 @@
@Resource默认按名称装配,当找不到与名称匹配的bean才会按类型装配。
-@PostConstruct 初始化注解
-
-@PreDestroy 摧毁注解 默认 单例 启动就加载
-
### Spring中的这几个注解有什么区别:@Component 、@Repository、@Service、@Controller
diff --git a/docs/basics/java-basic/bio-vs-nio-vs-aio.md b/docs/basics/java-basic/bio-vs-nio-vs-aio.md
index 9a976240..7349cd3d 100644
--- a/docs/basics/java-basic/bio-vs-nio-vs-aio.md
+++ b/docs/basics/java-basic/bio-vs-nio-vs-aio.md
@@ -1,5 +1,5 @@
### IO
-什么是IO? 它是指计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。
+什么是IO? 它是指计算机与外部世界或者一个程序与计算机的其余部分之间的接口。它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。
在 Java 编程中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。
@@ -25,7 +25,7 @@ Java AIO即Async非阻塞,是异步非阻塞的IO。
BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
-NIO (New I/O):同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
+NIO (New I/O):同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断地轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
AIO ( Asynchronous I/O):异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
@@ -225,4 +225,4 @@ public class WriteToFile {
}
}
-```
\ No newline at end of file
+```
diff --git a/docs/basics/java-basic/boxing-unboxing.md b/docs/basics/java-basic/boxing-unboxing.md
index 00d0aa36..84ae2fd8 100644
--- a/docs/basics/java-basic/boxing-unboxing.md
+++ b/docs/basics/java-basic/boxing-unboxing.md
@@ -282,7 +282,7 @@ Java SE 的自动拆装箱还提供了一个和缓存有关的功能,我们先
}
```
-我们普遍认为上面的两个判断的结果都是 false。虽然比较的值是相等的,但是由于比较的是对象,而对象的引用不一样,所以会认为两个 if 判断都是 false 的。在 Java 中,`==` 比较的是对象应用,而 `equals` 比较的是值。所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回 false。奇怪的是,这里两个类似的 if 条件判断返回不同的布尔值。
+我们普遍认为上面的两个判断的结果都是 false。虽然比较的值是相等的,但是由于比较的是对象,而对象的引用不一样,所以会认为两个 if 判断都是 false 的。在 Java 中,`==` 比较的是对象引用,而 `equals` 比较的是值。所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回 false。奇怪的是,这里两个类似的 if 条件判断返回不同的布尔值。
上面这段代码真正的输出结果:
diff --git a/docs/basics/java-basic/bug-in-apache-commons-collections.md b/docs/basics/java-basic/bug-in-apache-commons-collections.md
index e040cdc2..cdea3072 100644
--- a/docs/basics/java-basic/bug-in-apache-commons-collections.md
+++ b/docs/basics/java-basic/bug-in-apache-commons-collections.md
@@ -100,7 +100,7 @@ Commons Collections中提供了一个Transformer接口,主要是可以用来
![][7]
-那么,结合序列化,现在的攻击更加进了一步,不再需要一定要传入`newTransformer.transform(Runtime.getRuntime());`这样的代码了,只要代码中有 `transformer.transform()`方法的调用即可,无论里面是什么参数:
+那么,结合序列化,现在的攻击更加进了一步,不再需要传入`newTransformer.transform(Runtime.getRuntime());`这样的代码了,只要代码中有 `transformer.transform()`方法的调用即可,无论里面是什么参数:
![][8]
@@ -249,4 +249,4 @@ BadAttributeValueExpException类是Java中提供的一个异常类,他的readO
[12]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944537014741.jpg
[13]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944526874284.jpg
[14]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944525715616.jpg
- [15]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944525999226.jpg
\ No newline at end of file
+ [15]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944525999226.jpg
diff --git a/docs/basics/java-basic/bug-in-fastjson.md b/docs/basics/java-basic/bug-in-fastjson.md
index 503dc641..82219f3c 100644
--- a/docs/basics/java-basic/bug-in-fastjson.md
+++ b/docs/basics/java-basic/bug-in-fastjson.md
@@ -195,15 +195,15 @@ fastjson的主要功能就是将Java Bean序列化成JSON字符串,这样得
而黑白名单又是通过startWith检测的,那么黑客只要在自己想要使用的攻击类库前后加上`L`和`;`就可以绕过黑白名单的检查了,也不耽误被fastjson正常加载。
-如`Lcom.sun.rowset.JdbcRowSetImpl;`,会先通过白名单校验,然后fastjson在加载类的时候会去掉前后的`L`和`,变成了`com.sun.rowset.JdbcRowSetImpl`。
+如`Lcom.sun.rowset.JdbcRowSetImpl;`,会先通过白名单校验,然后fastjson在加载类的时候会去掉前后的`L`和`;`,变成了`com.sun.rowset.JdbcRowSetImpl`。
为了避免被攻击,在之后的 v1.2.42版本中,在进行黑白名单检测的时候,fastjson先判断目标类的类名的前后是不是`L`和`;`,如果是的话,就截取掉前后的`L`和`;`再进行黑白名单的校验。
看似解决了问题,但是黑客发现了这个规则之后,就在攻击时在目标类前后双写`LL`和`;;`,这样再被截取之后还是可以绕过检测。如`LLcom.sun.rowset.JdbcRowSetImpl;;`
-魔高一尺,道高一丈。在 v1.2.43中,fastjson这次在黑白名单判断之前,增加了一个是否以`LL`未开头的判断,如果目标类以`LL`开头,那么就直接抛异常,于是就又短暂的修复了这个漏洞。
+魔高一尺,道高一丈。在 v1.2.43中,fastjson这次在黑白名单判断之前,增加了一个是否以`LL`开头的判断,如果目标类以`LL`开头,那么就直接抛异常,于是就又短暂的修复了这个漏洞。
-黑客在`L`和`;`这里走不通了,于是想办法从其他地方下手,因为fastjson在加载类的时候,不只对`L`和`;`这样的类进行特殊处理,还对`[`也被特殊处理了。
+黑客在`L`和`;`这里走不通了,于是想办法从其他地方下手,因为fastjson在加载类的时候,不只对`L`和`;`这样的类进行特殊处理,还对`[`特殊处理了。
同样的攻击手段,在目标类前面添加`[`,v1.2.43以前的所有版本又沦陷了。
@@ -334,4 +334,4 @@ http://www.lmxspace.com/2019/06/29/FastJson-反序列化学习
[3]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938462506312.jpg
[4]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938495572144.jpg
[5]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938532891003.jpg
- [6]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938545656293.jpg
\ No newline at end of file
+ [6]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938545656293.jpg
diff --git a/docs/basics/java-basic/class-contant-pool.md b/docs/basics/java-basic/class-contant-pool.md
index 781dce6c..d55008c0 100644
--- a/docs/basics/java-basic/class-contant-pool.md
+++ b/docs/basics/java-basic/class-contant-pool.md
@@ -67,7 +67,7 @@ Class常量池可以理解为是Class文件中的资源仓库。 Class文件中
> 从上图中可以看到,反编译后的class文件常量池中共有16个常量。而Class文件中常量计数器的数值是0011,将该16进制数字转换成10进制的结果是17。
>
-> 原因是与Java的语言习惯不同,常量池计数器是从0开始而不是从1开始的,常量池的个数是10进制的17,这就代表了其中有16个常量,索引值范围为1-16。
+> 原因是与Java的语言习惯不同,常量池计数器是从1开始而不是从0开始的,常量池的个数是10进制的17,这就代表了其中有16个常量,索引值范围为1-16。
### 常量池中有什么
@@ -77,7 +77,7 @@ Class常量池可以理解为是Class文件中的资源仓库。 Class文件中
### 字面量
-前面说过,运行时常量池中主要保存的是字面量和符号引用,那么到底什么字面量?
+前面说过,Class常量池中主要保存的是字面量和符号引用,那么到底什么字面量?
> 在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。
@@ -127,4 +127,4 @@ Java代码在进行`Javac`编译的时候,并不像C和C++那样有“连接
[4]: http://www.hollischuang.com/archives/491
[5]: http://www.hollischuang.com/wp-content/uploads/2018/10/15401192359009.jpg
[6]: http://www.hollischuang.com/wp-content/uploads/2018/10/15401195127619.jpg
- [7]: https://blog.csdn.net/luanlouis/article/details/39960815
\ No newline at end of file
+ [7]: https://blog.csdn.net/luanlouis/article/details/39960815
diff --git a/docs/basics/java-basic/constructor.md b/docs/basics/java-basic/constructor.md
deleted file mode 100644
index a0b3c011..00000000
--- a/docs/basics/java-basic/constructor.md
+++ /dev/null
@@ -1,5 +0,0 @@
-构造函数,是一种特殊的方法。 主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。 特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。
-
-构造函数跟一般的实例方法十分相似;但是与其它方法不同,构造器没有返回类型,不会被继承,且可以有范围修饰符。构造器的函数名称必须和它所属的类的名称相同。 它承担着初始化对象数据成员的任务。
-
-如果在编写一个可实例化的类时没有专门编写构造函数,多数编程语言会自动生成缺省构造器(默认构造函数)。默认构造函数一般会把成员变量的值初始化为默认值,如int -> 0,Integer -> null。
diff --git a/docs/basics/java-basic/diff-serializable-vs-externalizable.md b/docs/basics/java-basic/diff-serializable-vs-externalizable.md
index 5b2ed9a7..7e94fc59 100644
--- a/docs/basics/java-basic/diff-serializable-vs-externalizable.md
+++ b/docs/basics/java-basic/diff-serializable-vs-externalizable.md
@@ -1,17 +1,17 @@
-Java中的类通过实现 `java.io.Serializable` 接口以启⽤其序列化功能。 未实现此接⼜的类将⽆法使其任何状态序列化或反序列化。
+Java中的类通过实现 `java.io.Serializable` 接口以启⽤其序列化功能。 未实现此接口的类将⽆法使其任何状态序列化或反序列化。
可序列化类的所有⼦类型本⾝都是可序列化的。
-序列化接⼜没有⽅法或字段, 仅⽤于标识可序列化的语义。
+序列化接口没有⽅法或字段, 仅⽤于标识可序列化的语义。
当试图对⼀个对象进⾏序列化的时候, 如果遇到不⽀持`Serializable` 接口的对象。 在此情况下, 将抛`NotSerializableException`。
如果要序列化的类有⽗类, 要想同时将在⽗类中定义过的变量持久化下来, 那么⽗类也应该集成`java.io.Serializable`接口。
-`Externalizable`继承了`Serializable`, 该接⼜中定义了两个抽象⽅法:`writeExternal()`与`readExternal()`。 当使⽤`Externalizable`接口来进⾏序列化与反序列化的时候需要开发⼈员重写writeExternal()与readExternal()⽅法。
+`Externalizable`继承了`Serializable`, 该接口中定义了两个抽象⽅法:`writeExternal()`与`readExternal()`。 当使⽤`Externalizable`接口来进⾏序列化与反序列化的时候需要开发⼈员重写writeExternal()与readExternal()⽅法。
如果没有在这两个⽅法中定义序列化实现细节, 那么序列化之后, 对象内容为空。
-实现`Externalizable`接⼜的类必须要提供⼀个`public`的⽆参的构造器。
+实现`Externalizable`接口的类必须要提供⼀个`public`的⽆参的构造器。
所以, 实现`Externalizable`, 并实现`writeExternal()`和`readExternal()`⽅法可以指定序列化哪些属性。
diff --git a/docs/basics/java-basic/dynamic-proxy-implementation.md b/docs/basics/java-basic/dynamic-proxy-implementation.md
index 1ca4889a..6f5c7e1c 100644
--- a/docs/basics/java-basic/dynamic-proxy-implementation.md
+++ b/docs/basics/java-basic/dynamic-proxy-implementation.md
@@ -6,7 +6,8 @@ Java中,实现动态代理有两种方式:
关于这两种动态代理的写法本文就不深入展开了,读者感兴趣的话,后面我再写文章单独介绍。本文主要来简单说一下这两种动态代理的区别和用途。
-JDK动态代理和Cglib动态代理的区别
+JDK动态代理和Cglib动态代理的区别
+
JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口。如果想代理没有实现接口的类,就可以使用CGLIB实现。
Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。
@@ -120,4 +121,4 @@ public class DoCGLib {
proxyImp.add();
}
}
-```
\ 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-impl.md b/docs/basics/java-basic/enum-impl.md
index 51a18c1f..190cdd67 100644
--- a/docs/basics/java-basic/enum-impl.md
+++ b/docs/basics/java-basic/enum-impl.md
@@ -40,6 +40,6 @@ Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将
}
}
-通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。
+通过反编译代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。
-当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
\ No newline at end of file
+当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
diff --git a/docs/basics/java-basic/enum-serializable.md b/docs/basics/java-basic/enum-serializable.md
index b588b37e..ff85c4d6 100644
--- a/docs/basics/java-basic/enum-serializable.md
+++ b/docs/basics/java-basic/enum-serializable.md
@@ -91,7 +91,7 @@
**2\. 枚举自己处理序列化**
-> 我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,**为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。**原文如下:
+> 我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例的了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,**为了保证枚举类型像Java规范中所说的那样,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。**原文如下:
>
> > Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant's name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant's enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored--all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.
>
diff --git a/docs/basics/java-basic/enum-singleton.md b/docs/basics/java-basic/enum-singleton.md
index 146fc3f0..05164b23 100644
--- a/docs/basics/java-basic/enum-singleton.md
+++ b/docs/basics/java-basic/enum-singleton.md
@@ -2,12 +2,12 @@
单例相关文章一览:
-[设计模式(二)——单例模式][1]
-[设计模式(三)——JDK中的那些单例][2]
-[单例模式的七种写法][3]
-[单例与序列化的那些事儿][4]
-[不使用synchronized和lock,如何实现一个线程安全的单例?][5]
-[不使用synchronized和lock,如何实现一个线程安全的单例?(二)][6]
+[设计模式(二)——单例模式][1]
+[设计模式(三)——JDK中的那些单例][2]
+[单例模式的七种写法][3]
+[单例与序列化的那些事儿][4]
+[不使用synchronized和lock,如何实现一个线程安全的单例?][5]
+[不使用synchronized和lock,如何实现一个线程安全的单例?(二)][6]
如果你对单例不是很了解,或者对于单例的线程安全问题以及序列化会破坏单例等问题不是很清楚,可以先阅读以上文章。上面六篇文章看完之后,相信你一定会对单例模式有更多,更深入的理解。
@@ -15,7 +15,7 @@
### 哪种写单例的方式最好
-在StakcOverflow中,有一个关于[What is an efficient way to implement a singleton pattern in Java?][7]的讨论:
+在StackOverflow中,有一个关于[What is an efficient way to implement a singleton pattern in Java?][7]的讨论:
@@ -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;
}
}
@@ -78,7 +78,7 @@
通过将定义好的枚举[反编译][9],我们就能发现,其实枚举在经过`javac`的编译之后,会被转换成形如`public final class T extends Enum`的定义。
-而且,枚举中的各个枚举项同事通过`static`来定义的。如:
+而且,枚举中的各个枚举项同时通过`static`来定义的。如:
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
@@ -145,4 +145,4 @@
[9]: http://www.hollischuang.com/archives/58
[10]: http://www.hollischuang.com/archives/199
[11]: http://www.hollischuang.com/archives/201
- [12]: https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serial-arch.html#6469
\ No newline at end of file
+ [12]: https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serial-arch.html#6469
diff --git a/docs/basics/java-basic/enum-usage.md b/docs/basics/java-basic/enum-usage.md
index 32c7029c..3e27f168 100644
--- a/docs/basics/java-basic/enum-usage.md
+++ b/docs/basics/java-basic/enum-usage.md
@@ -42,9 +42,9 @@
}
-程序`getChineseSeason(Season.SPRING)`是我们预期的使用方法。可`getChineseSeason(5)`显然就不是了,而且编译很通过,在运行时会出现什么情况,我们就不得而知了。这显然就不符合`Java`程序的类型安全。
+程序`getChineseSeason(Season.SPRING)`是我们预期的使用方法。可`getChineseSeason(5)`显然就不是了,而且编译会通过,在运行时会出现什么情况,我们就不得而知了。这显然就不符合`Java`程序的类型安全。
-接下来我们来考虑一下这种模式的**可读性**。使用枚举的大多数场合,我都需要方便得到枚举类型的字符串表达式。如果将`int`枚举常量打印出来,我们所见到的就是一组数字,这是没什么太大的用处。我们可能会想到使用`String`常量代替`int`常量。虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作,所以这种模式也是我们不期望的。 从**类型安全性**和**程序可读性**两方面考虑,`int`和`String`枚举模式的缺点就显露出来了。幸运的是,从`Java1.5`发行版本开始,就提出了另一种可以替代的解决方案,可以避免`int`和`String`枚举模式的缺点,并提供了许多额外的好处。那就是枚举类型(`enum type`)。接下来的章节将介绍枚举类型的定义、特征、应用场景和优缺点。
+接下来我们来考虑一下这种模式的**可读性**。使用枚举的大多数场合,我都需要方便得到枚举类型的字符串表达式。如果将`int`枚举常量打印出来,我们所见到的就是一组数字,这没什么太大的用处。我们可能会想到使用`String`常量代替`int`常量。虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作,所以这种模式也是我们不期望的。 从**类型安全性**和**程序可读性**两方面考虑,`int`和`String`枚举模式的缺点就显露出来了。幸运的是,从`Java1.5`发行版本开始,就提出了另一种可以替代的解决方案,可以避免`int`和`String`枚举模式的缺点,并提供了许多额外的好处。那就是枚举类型(`enum type`)。接下来的章节将介绍枚举类型的定义、特征、应用场景和优缺点。
### 2 定义
diff --git a/docs/basics/java-basic/fail-fast-vs-fail-safe.md b/docs/basics/java-basic/fail-fast-vs-fail-safe.md
index d425685e..c1917387 100644
--- a/docs/basics/java-basic/fail-fast-vs-fail-safe.md
+++ b/docs/basics/java-basic/fail-fast-vs-fail-safe.md
@@ -11,14 +11,14 @@
举一个最简单的fail-fast的例子:
public int divide(int divisor,int dividend){
- if(dividend == 0){
- throw new RuntimeException("dividend can't be null");
+ if(divisor == 0){
+ throw new RuntimeException("divisor can't be null");
}
- return divisor/dividend;
+ return dividend/divisor;
}
-上面的代码是一个对两个整数做除法的方法,在divide方法中,我们对被除数做了个简单的检查,如果其值为0,那么就直接抛出一个异常,并明确提示异常原因。这其实就是fail-fast理念的实际应用。
+上面的代码是一个对两个整数做除法的方法,在divide方法中,我们对除数做了个简单的检查,如果其值为0,那么就直接抛出一个异常,并明确提示异常原因。这其实就是fail-fast理念的实际应用。
这样做的好处就是可以预先识别出一些错误情况,一方面可以避免执行复杂的其他代码,另外一方面,这种异常情况被识别之后也可以针对性的做一些单独处理。
@@ -111,9 +111,9 @@ CMException,当方法检测到对象的并发修改,但不允许这种修改
}
-如上,在该方法中对modCount和expectedModCount进行了比较,如果二者不想等,则抛出CMException。
+如上,在该方法中对modCount和expectedModCount进行了比较,如果二者不相等,则抛出CMException。
-那么,modCount和expectedModCount是什么?是什么原因导致他们的值不想等的呢?
+那么,modCount和expectedModCount是什么?是什么原因导致他们的值不相等的呢?
modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。
@@ -257,4 +257,4 @@ CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为
**所以CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。**而Vector在读写的时候使用同一个容器,读写互斥,同时只能做一件事儿。
[1]: https://www.hollischuang.com/archives/58
- [2]: https://www.hollischuang.com/wp-content/uploads/2019/04/15551448234429.jpg
\ No newline at end of file
+ [2]: https://www.hollischuang.com/wp-content/uploads/2019/04/15551448234429.jpg
diff --git a/docs/basics/java-basic/final-string.md b/docs/basics/java-basic/final-string.md
index 3f7aade7..a2e2e05b 100644
--- a/docs/basics/java-basic/final-string.md
+++ b/docs/basics/java-basic/final-string.md
@@ -1,39 +1,109 @@
+String在Java中特别常用,而且我们经常要在代码中对字符串进行赋值和改变他的值,但是,为什么我们说字符串是不可变的呢?
-* * *
+首先,我们需要知道什么是不可变对象?
-## 定义一个字符串
+不可变对象是在完全创建后其内部状态保持不变的对象。这意味着,一旦对象被赋值给变量,我们既不能更新引用,也不能通过任何方式改变内部状态。
- String s = "abcd";
-
+可是有人会有疑惑,String为什么不可变,我的代码中经常改变String的值啊,如下:
-![String-Immutability-1][1]
+```
+String s = "abcd";
+s = s.concat("ef");
-`s`中保存了string对象的引用。下面的箭头可以理解为“存储他的引用”。
+```
-## 使用变量来赋值变量
- String s2 = s;
-
+这样,操作,不就将原本的"abcd"的字符串改变成"abcdef"了么?
-![String-Immutability-2][2]
+但是,虽然字符串内容看上去从"abcd"变成了"abcdef",但是实际上,我们得到的已经是一个新的字符串了。
-s2保存了相同的引用值,因为他们代表同一个对象。
+![][1]
-## 字符串连接
+如上图,在堆中重新创建了一个"abcdef"字符串,和"abcd"并不是同一个对象。
- s = s.concat("ef");
-
+所以,一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。而且,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
-![string-immutability][3]
+如果我们想要一个可修改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。
-`s`中保存的是一个重新创建出来的string对象的引用。
+### 为什么String要设计成不可变
-## 总结
+在知道了"String是不可变"的之后,大家是不是一定都很疑惑:为什么要把String设计成不可变的呢?有什么好处呢?
-一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
+这个问题,困扰过很多人,甚至有人直接问过Java的创始人James Gosling。
-如果你需要一个可修改的字符串,应该使用StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的string对象被创建出来。
+在一次采访中James Gosling被问到什么时候应该使用不可变变量,他给出的回答是:
- [1]: http://www.programcreek.com/wp-content/uploads/2009/02/String-Immutability-1.jpeg
- [2]: http://www.programcreek.com/wp-content/uploads/2009/02/String-Immutability-2.jpeg
- [3]: http://www.programcreek.com/wp-content/uploads/2009/02/string-immutability-650x279.jpeg
\ No newline at end of file
+> I would use an immutable whenever I can.
+
+那么,他给出这个答案背后的原因是什么呢?是基于哪些思考的呢?
+
+其实,主要是从缓存、安全性、线程安全和性能等角度触发的。
+
+Q:缓存、安全性、线程安全和性能?这有都是啥
+A:你别急,听我一个一个给你讲就好了。
+
+#### 缓存
+
+字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。
+
+JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串池。
+
+通过字符串池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。
+
+```
+String s = "abcd";
+String s2 = s;
+```
+
+
+对于这个例子,s和s2都表示"abcd",所以他们会指向字符串池中的同一个字符串对象:
+
+![][2]
+
+但是,之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动的改变了,这显然不是我们想看到的。
+
+#### 安全性
+
+字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。
+
+因此,保护String类对于提升整个应用程序的安全性至关重要。
+
+当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。
+
+但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。
+
+#### 线程安全
+
+不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。
+
+因此,一般来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的,因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值。因此,字符串对于多线程来说是安全的。
+
+#### hashcode缓存
+
+由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行操作时,经常调用hashCode()方法。
+
+不可变性保证了字符串的值不会改变。因此,hashCode()方法在String类中被重写,以方便缓存,这样在第一次hashCode()调用期间计算和缓存散列,并从那时起返回相同的值。
+
+在String类中,有以下代码:
+
+```
+private int hash;//this is used to cache hash code.
+```
+
+
+#### 性能
+
+前面提到了的字符串池、hashcode缓存等,都是提升性能的提现。
+
+因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对hashcode进行缓存,更加高效
+
+由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。
+
+### 总结
+
+通过本文,我们可以得出这样的结论:字符串是不可变的,因此它们的引用可以被视为普通变量,可以在方法之间和线程之间传递它们,而不必担心它所指向的实际字符串对象是否会改变。
+
+我们还了解了促使Java语言设计人员将该类设置为不可变类的其他原因。主要考虑的是缓存、安全性、线程安全和性能等方面
+
+ [1]: https://www.hollischuang.com/wp-content/uploads/2021/03/16163108328434.jpg
+ [2]: https://www.hollischuang.com/wp-content/uploads/2021/03/16163114985563.jpg
\ No newline at end of file
diff --git a/docs/basics/java-basic/float.md b/docs/basics/java-basic/float.md
index f7cf14af..c1418645 100644
--- a/docs/basics/java-basic/float.md
+++ b/docs/basics/java-basic/float.md
@@ -1,11 +1,53 @@
-在计算机科学中,浮点是一种对于实数的近似值数值表现法,由一个有效数字(即尾数)加上幂数来表示,通常是乘以某个基数的整数次指数得到。以这种表示法表示的数值,称为浮点数(floating-point number)。
+我们知道,计算机的数字的存储和运算都是通过二进制进行的,对于,十进制整数转换为二进制整数采用"除2取余,逆序排列"法
-计算机使用浮点数运算的主因,在于电脑使用二进位制的运算。例如:4÷2=2,4的二进制表示为100、2的二进制表示为010,在二进制中,相当于退一位数(100 -> 010)。
+具体做法是:
-1的二进制是01,1.0/2=0.5,那么,0.5的二进制表示应该为(0.1),以此类推,0.25的二进制表示为0.01,所以,并不是说所有的十进制小数都能准确的用二进制表示出来,如0.1,因此只能使用近似值的方式表达。
+* 用2整除十进制整数,可以得到一个商和余数;
+* 再用2去除商,又会得到一个商和余数,如此进行,直到商为小于1时为止
+* 然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。
-也就是说,,十进制的小数在计算机中是由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到的,这种表示方法类似于基数为10的科学计数法。
+如,我们想要把127转换成二进制,做法如下:
-一个浮点数a由两个数m和e来表示:a = m × be。在任意一个这样的系统中,我们选择一个基数b(记数系统的基)和精度p(即使用多少位来存储)。m(即尾数)是形如±d.ddd...ddd的p位数(每一位是一个介于0到b-1之间的整数,包括0和b-1)。如果m的第一位是非0整数,m称作正规化的。有一些描述使用一个单独的符号位(s 代表+或者-)来表示正负,这样m必须是正的。e是指数。
+
-位(bit)是衡量浮点数所需存储空间的单位,通常为32位或64位,分别被叫作单精度和双精度。
\ No newline at end of file
+那么,十进制小数转换成二进制小数,又该如何计算呢?
+
+
+十进制小数转换成二进制小数采用"乘2取整,顺序排列"法。
+
+具体做法是:
+
+* 用2乘十进制小数,可以得到积
+* 将积的整数部分取出,再用2乘余下的小数部分,又得到一个积
+* 再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止。
+
+
+如尝试将0.625转成二进制:
+
+
+
+但是0.625是一个特列,用同样的算法,请计算下0.1对应的二进制是多少:
+
+
+
+我们发现,0.1的二进制表示中出现了无限循环的情况,也就是(0.1)10 = (0.000110011001100…)2
+
+这种情况,计算机就没办法用二进制精确的表示0.1了。
+
+所以,为了解决部分小数无法使用二进制精确表示的问题,于是就有了IEEE 754规范。
+
+IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。
+
+>浮点数和小数并不是完全一样的,计算机中小数的表示法,其实有定点和浮点两种。因为在位数相同的情况下,定点数的表示范围要比浮点数小。所以在计算机科学中,使用浮点数来表示实数的近似值。
+
+IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。
+
+其中最常用的就是32位单精度浮点数和64位双精度浮点数。
+
+IEEE并没有解决小数无法精确表示的问题,只是提出了一种使用近似值表示小数的方式,并且引入了精度的概念。
+
+一个浮点数a由两个数m和e来表示:a = m × b^e。
+
+在任意一个这样的系统中,我们选择一个基数b(记数系统的基)和精度p(即使用多少位来存储)。m(即尾数)是形如±d.ddd...ddd的p位数(每一位是一个介于0到b-1之间的整数,包括0和b-1)。
+
+如果m的第一位是非0整数,m称作规格化的。有一些描述使用一个单独的符号位(s 代表+或者-)来表示正负,这样m必须是正的。e是指数。
\ No newline at end of file
diff --git a/docs/basics/java-basic/generics-problem.md b/docs/basics/java-basic/generics-problem.md
index ccbd2ab6..0a99bea7 100644
--- a/docs/basics/java-basic/generics-problem.md
+++ b/docs/basics/java-basic/generics-problem.md
@@ -1,6 +1,6 @@
-###一、当泛型遇到重载
+### 一、当泛型遇到重载
public class GenericTypes {
diff --git a/docs/basics/java-basic/hash-in-hashmap.md b/docs/basics/java-basic/hash-in-hashmap.md
index 00e14542..4cc4f7e6 100644
--- a/docs/basics/java-basic/hash-in-hashmap.md
+++ b/docs/basics/java-basic/hash-in-hashmap.md
@@ -51,7 +51,7 @@
### 源码解析
-首先,在同一个版本的Jdk中,HashMap、HashTable以及ConcurrentHashMap里面的hash方法的实现是不同的。再不同的版本的JDK中(Java7 和 Java8)中也是有区别的。我会尽量全部介绍到。相信,看文这篇文章,你会彻底理解hash方法。
+首先,在同一个版本的Jdk中,HashMap、HashTable以及ConcurrentHashMap里面的hash方法的实现是不同的。在不同的版本的JDK中(Java7 和 Java8)中也是有区别的。我会尽量全部介绍到。相信,看完这篇文章,你会彻底理解hash方法。
在上代码之前,我们先来做个简单分析。我们知道,hash方法的功能是根据Key来定位这个K-V在链表数组中的位置的。也就是hash方法的输入应该是个Object类型的Key,输出应该是个int类型的数组下标。如果让你设计这个方法,你会怎么做?
@@ -79,7 +79,7 @@
}
-前面我说过,`indexFor`方法其实主要是将hash生成的整型转换成链表数组中的下标。那么`return h & (length-1);`是什么意思呢?其实,他就是取模。Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。**位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。**
+前面我说过,`indexFor`方法其实主要是将hash生成的整型转换成链表数组中的下标。那么`return h & (length-1);`是什么意思呢?其实,他就是取模。Java之所以使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。**位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。**
那么,为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下:
@@ -156,7 +156,7 @@ HashMap的数据是存储在链表数组里面的。在对HashMap进行插入/
>
> 也就是说,HashTable的链表数组的默认大小是一个素数、奇数。之后的每次扩充结果也都是奇数。
>
-> 由于HashTable会尽量使用素数、奇数作为容量的大小。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。(这个是可以证明出来的,由于不是本文重点,暂不详细介绍,可参考:http://zhaox.github.io/algorithm/2015/06/29/hash)
+> 由于HashTable会尽量使用素数、奇数作为容量的大小。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。(这个是可以证明出来的,由于不是本文重点,暂不详细介绍,可参考:http://zhaox.github.io/algorithm/2015/06/29/hash
至此,我们看完了Java 7中HashMap和HashTable中对于hash的实现,我们来做个简单的总结。
@@ -210,7 +210,7 @@ HashMap的数据是存储在链表数组里面的。在对HashMap进行插入/
HashTable In Java 8
-在Java 8的HashTable中,已经不在有hash方法了。但是哈希的操作还是在的,比如在put方法中就有如下实现:
+在Java 8的HashTable中,已经不再有hash方法了。但是哈希的操作还是在的,比如在put方法中就有如下实现:
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@@ -252,4 +252,4 @@ Jdk的源代码,每一行都很有意思,都值得花时间去钻研、推
[7]: https://www.jianshu.com/p/7e7f52a49ffc
[8]: http://blog.csdn.net/justloveyou_/article/details/62893086
[9]: http://zhaox.github.io/2016/07/05/hashmap-vs-hashtable
- [10]: https://www.zhihu.com/question/51784530
\ No newline at end of file
+ [10]: https://www.zhihu.com/question/51784530
diff --git a/docs/basics/java-basic/hashmap-capacity.md b/docs/basics/java-basic/hashmap-capacity.md
index 31042370..e6fb9eef 100644
--- a/docs/basics/java-basic/hashmap-capacity.md
+++ b/docs/basics/java-basic/hashmap-capacity.md
@@ -71,7 +71,7 @@ HashMap中的size和capacity之间的区别其实解释起来也挺简单的。
System.out.println("capacity : " + capacity.invoke(map));
-分别执行以上3段代码,分别输出:**capacity : 2、capacity : 8、capacity : 16**。
+分别执行以上3段代码,分别输出:**capacity : 1、capacity : 8、capacity : 16**。
也就是说,默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(1->1、7->8、9->16)
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/hashmap-default-loadfactor.md b/docs/basics/java-basic/hashmap-default-loadfactor.md
index 2f88bdd2..9a2f56b3 100644
--- a/docs/basics/java-basic/hashmap-default-loadfactor.md
+++ b/docs/basics/java-basic/hashmap-default-loadfactor.md
@@ -20,7 +20,7 @@
首先我们来介绍下什么是负载因子(loadFactory),如果读者对这部分已经有了解,那么可以直接跨过这一段。
-我们知道,第一次创建HashMap的时候,就会指定其容量(如果未显示制定,默认是16,详见[为啥HashMap的默认容量是16?][3]),那随着我们不断的向HashMap中put元素的时候,就有可能会超过其容量,那么就需要有一个扩容机制。
+我们知道,第一次创建HashMap的时候,就会指定其容量(如果未显示指定,默认是16,详见[为啥HashMap的默认容量是16?][3]),那随着我们不断的向HashMap中put元素的时候,就有可能会超过其容量,那么就需要有一个扩容机制。
所谓扩容,就是扩大HashMap的容量:
@@ -37,14 +37,14 @@
在HashMap中,临界值(threshold) = 负载因子(loadFactor) * 容量(capacity)。
-loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容。(相见[HashMap中傻傻分不清楚的那些概念][4])
+loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容。(详见[HashMap中傻傻分不清楚的那些概念][4])
### 为什么要扩容
还记得前面我们说过,HashMap在扩容到过程中不仅要对其容量进行扩充,还需要进行rehash!所以,这个过程其实是很耗时的,并且Map中元素越多越耗时。
-rehash的过程相当于对其中所有的元素重新做一遍hash,重新计算要分配到那个桶中。
+rehash的过程相当于对其中所有的元素重新做一遍hash,重新计算要分配到哪个桶中。
那么,有没有人想过一个问题,既然这么麻烦,为啥要扩容?HashMap不是一个数组链表吗?不扩容的话,也是可以无限存储的呀。为啥要扩容?
@@ -66,7 +66,7 @@ HashMap将数组和链表组合在一起,发挥了两者的优势,我们可
HashMap基于链表的数组的数据结构实现的
-我们在向HashMap中put元素的时候,就需要先定外到是数组中的哪条链表,然后把这个元素挂在这个链表的后面。
+我们在向HashMap中put元素的时候,就需要先定位到是数组中的哪条链表,然后把这个元素挂在这个链表的后面。
当我们从HashMap中get元素的时候,也是需要定位到是数组中的哪条链表,然后再逐一遍历链表中的元素,直到查找到需要的元素为止。
@@ -86,13 +86,13 @@ HashMap基于链表的数组的数据结构实现的
无外乎两种情况:
-1、容量太小。容量小,碰撞的概率就高了。狼多肉少,就会发生争强。
+1、容量太小。容量小,碰撞的概率就高了。狼多肉少,就会发生争抢。
-2、hash算法不够好。算法不合理,就可能都分到同一个或几个桶中。分配不均,也会发生争强。
+2、hash算法不够好。算法不合理,就可能都分到同一个或几个桶中。分配不均,也会发生争抢。
所以,解决HashMap中的哈希碰撞也是从这两方面入手。
-这两点在HashMap中都有很好的提现。两种方法相结合,**在合适的时候扩大数组容量,再通过一个合适的hash算法计算元素分配到哪个数组中,就可以大大的减少冲突的概率。就能避免查询效率低下的问题。**
+这两点在HashMap中都有很好的体现。两种方法相结合,**在合适的时候扩大数组容量,再通过一个合适的hash算法计算元素分配到哪个数组中,就可以大大的减少冲突的概率。就能避免查询效率低下的问题。**
### 为什么默认loadFactory是0.75
@@ -145,7 +145,7 @@ HashMap基于链表的数组的数据结构实现的
所以,合理值大概在0.7左右。
-当然,这个数学计算方法,并不是在Java的官方文档中提现的,我们也无从考察到底有没有这层考虑,就像我们根本不知道鲁迅写文章时候怎么想的一样,只能推测。这个推测来源于Stack Overflor(https://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmap)
+当然,这个数学计算方法,并不是在Java的官方文档中体现的,我们也无从考察到底有没有这层考虑,就像我们根本不知道鲁迅写文章时候怎么想的一样,只能推测。这个推测来源于Stack Overflor(https://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmap)
#### 0\.75的必然因素
@@ -180,7 +180,7 @@ HashMap是一种K-V结构,为了提升其查询及插入的速度,底层采
loadFactory太大,比如等于1,那么就会有很高的哈希冲突的概率,会大大降低查询速度。
-loadFactory太小,比如等于0.5,那么频繁扩容没,就会大大浪费空间。
+loadFactory太小,比如等于0.5,那么频繁扩容,就会大大浪费空间。
所以,这个值需要介于0.5和1之间。根据数学公式推算。这个值在log(2)的时候比较合理。
@@ -208,4 +208,4 @@ https://preshing.com/20110504/hash-collision-probabilities/
[4]: http://www.hollischuang.com/archives/2416
[5]: http://www.hollischuang.com/archives/2091
[6]: http://www.hollischuang.com/wp-content/uploads/2020/02/15823447916666.jpg
- [7]: http://www.hollischuang.com/wp-content/uploads/2020/02/15823459128857.jpg
\ No newline at end of file
+ [7]: http://www.hollischuang.com/wp-content/uploads/2020/02/15823459128857.jpg
diff --git a/docs/basics/java-basic/hashmap-init-capacity.md b/docs/basics/java-basic/hashmap-init-capacity.md
index afe3b496..2e27a749 100644
--- a/docs/basics/java-basic/hashmap-init-capacity.md
+++ b/docs/basics/java-basic/hashmap-init-capacity.md
@@ -24,7 +24,7 @@
**但是,这么做不仅不对,而且以上方式创建出来的Map的容量也不是7。**
-因为,当我们使用HashMap(int initialCapacity)来初始化容量的时候,HashMap并不会使用我们传进来的initialCapacity直接作为初识容量。
+因为,当我们使用HashMap(int initialCapacity)来初始化容量的时候,HashMap并不会使用我们传进来的initialCapacity直接作为初始容量。
**JDK会默认帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个比用户传入的值大的2的幂。**
@@ -83,4 +83,4 @@
或者哪一天你碰到一个面试官问你一些细节的时候,你也能有个印象,或者某一天你也可以拿这个出去面试问其他人~!啊哈哈哈。
[1]: http://www.hollischuang.com/archives/2416
- [2]: http://www.hollischuang.com/wp-content/uploads/2019/12/15756974111211.jpg
\ No newline at end of file
+ [2]: http://www.hollischuang.com/wp-content/uploads/2019/12/15756974111211.jpg
diff --git a/docs/basics/java-basic/inheritance-composition.md b/docs/basics/java-basic/inheritance-composition.md
deleted file mode 100644
index 5eff0d32..00000000
--- a/docs/basics/java-basic/inheritance-composition.md
+++ /dev/null
@@ -1,67 +0,0 @@
-Java是一个面向对象的语言。每一个学习过Java的人都知道,封装、继承、多态是面向对象的三个特征。每个人在刚刚学习继承的时候都会或多或少的有这样一个印象:继承可以帮助我实现类的复用。所以,很多开发人员在需要复用一些代码的时候会很自然的使用类的继承的方式,因为书上就是这么写的(老师就是这么教的)。但是,其实这样做是不对的。长期大量的使用继承会给代码带来很高的维护成本。
-
-本文将介绍组合和继承的概念及区别,并从多方面分析在写代码时如何进行选择。
-
-## 面向对象的复用技术
-
-前面提到复用,这里就简单介绍一下面向对象的复用技术。
-
-复用性是面向对象技术带来的很棒的潜在好处之一。如果运用的好的话可以帮助我们节省很多开发时间,提升开发效率。但是,如果被滥用那么就可能产生很多难以维护的代码。
-
-作为一门面向对象开发的语言,代码复用是Java引人注意的功能之一。Java代码的复用有继承,组合以及代理三种具体的表现形式。本文将重点介绍继承复用和组合复用。
-
-## 继承
-
-继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;继承是一种[`is-a`][1]关系。(图片来自网络,侵删。)
-
-![Inheritance][2]
-
-## 组合
-
-组合(Composition)体现的是整体与部分、拥有的关系,即[`has-a`][3]的关系。
-
-![Composition][4]
-
-## 组合与继承的区别和联系
-
-> 在`继承`结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种`白盒式代码复用`。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;)
->
-> `组合`是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是`黑盒式代码复用`。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)
->
-> `继承`,在写代码的时候就要指名具体继承哪个类,所以,在`编译期`就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)
->
-> `组合`,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在`运行期`确定。
-
-## 优缺点对比
-
-| 组 合 关 系 | 继 承 关 系 |
-| -------------------------------- | -------------------------------------- |
-| 优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 | 缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性 |
-| 优点:具有较好的可扩展性 | 缺点:支持扩展,但是往往以增加系统结构的复杂度为代价 |
-| 优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 | 缺点:不支持动态继承。在运行时,子类无法选择不同的父类 |
-| 优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 | 缺点:子类不能改变父类的接口 |
-| 缺点:整体类不能自动获得和局部类同样的接口 | 优点:子类能自动继承父类的接口 |
-| 缺点:创建整体类的对象时,需要创建所有局部类的对象 | 优点:创建子类的对象时,无须创建父类的对象 |
-
-## 如何选择
-
-相信很多人都知道面向对象中有一个比较重要的原则『多用组合、少用继承』或者说『组合优于继承』。从前面的介绍已经优缺点对比中也可以看出,组合确实比继承更加灵活,也更有助于代码维护。
-
-所以,
-
-> **`建议在同样可行的情况下,优先使用组合而不是继承。`**
->
-> **`因为组合更安全,更简单,更灵活,更高效。`**
-
-注意,并不是说继承就一点用都没有了,前面说的是【在同样可行的情况下】。有一些场景还是需要使用继承的,或者是更适合使用继承。
-
-> 继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。《[Java编程思想][5]》
->
-> 只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在[`is-a`][1]关系的时候,类B才应该继承类A。《[Effective Java][6]》
-
- [1]: https://zh.wikipedia.org/wiki/Is-a
- [2]: http://www.hollischuang.com/wp-content/uploads/2016/03/Generalization.jpg
- [3]: https://en.wikipedia.org/wiki/Has-a
- [4]: http://www.hollischuang.com/wp-content/uploads/2016/03/Composition.jpg
- [5]: http://s.click.taobao.com/t?e=m%3D2%26s%3DHzJzud6zOdocQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67vo5P8BMUBgoEC56fBbgyn5pS4hLH%2FP02ckKYNRBWOBBey11vvWwHXSniyi5vWXIZhtlrJbLMDAQihpQCXu2JnPFYKQlNeOGCsYMXU3NNCg%2F&pvid=10_125.119.86.125_222_1458652212179
- [6]: http://s.click.taobao.com/t?e=m%3D2%26s%3DwIPn8%2BNPqLwcQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67vo5P8BMUBgoUOZr0mLjusdpS4hLH%2FP02ckKYNRBWOBBey11vvWwHXSniyi5vWXIZvgXwmdyquYbNLnO%2BjzYQLqKnzbV%2FMLqnMYMXU3NNCg%2F&pvid=10_125.119.86.125_345_1458652241780
diff --git a/docs/basics/java-basic/integer-cache.md b/docs/basics/java-basic/integer-cache.md
index 9b47ceea..66be41db 100644
--- a/docs/basics/java-basic/integer-cache.md
+++ b/docs/basics/java-basic/integer-cache.md
@@ -27,7 +27,7 @@
}
-我们普遍认为上面的两个判断的结果都是false。虽然比较的值是相等的,但是由于比较的是对象,而对象的引用不一样,所以会认为两个if判断都是false的。在Java中,`==`比较的是对象应用,而`equals`比较的是值。所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回false。奇怪的是,这里两个类似的if条件判断返回不同的布尔值。
+我们普遍认为上面的两个判断的结果都是false。虽然比较的值是相等的,但是由于比较的是对象,而对象的引用不一样,所以会认为两个if判断都是false的。在Java中,`==`比较的是对象引用,而`equals`比较的是值。所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回false。奇怪的是,这里两个类似的if条件判断返回不同的布尔值。
上面这段代码真正的输出结果:
@@ -161,4 +161,4 @@ IntegerCache是Integer类中定义的一个`private static`的内部类。接下
[2]: http://www.hollischuang.com/?p=1174
[3]: http://javapapers.com/
[4]: http://www.hollischuang.com
- [5]: http://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.7
\ No newline at end of file
+ [5]: http://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.7
diff --git a/docs/basics/java-basic/integer-scope.md b/docs/basics/java-basic/integer-scope.md
index 7ed5f264..6fc1c9e3 100644
--- a/docs/basics/java-basic/integer-scope.md
+++ b/docs/basics/java-basic/integer-scope.md
@@ -1,11 +1,13 @@
Java中的整型主要包含byte、short、int和long这四种,表示的数字范围也是从小到大的,之所以表示范围不同主要和他们存储数据时所占的字节数有关。
-先来个简答的科普,1字节=8位(bit)。java中的整型属于有符号数。
+先来个简单的科普,1字节=8位(bit)。java中的整型属于有符号数。
先来看计算中8bit可以表示的数字:
+
最小值:10000000 (-128)(-2^7)
+
最大值:01111111(127)(2^7-1)
-具体计算方式参考:[Java中,为什么byte类型的取值范围为-128~127? - CSDN博客](https://blog.csdn.net/qq_23418393/article/details/57421688)
+
整型的这几个类型中,
diff --git a/docs/basics/java-basic/ioc-implement-with-factory-and-reflection.md b/docs/basics/java-basic/ioc-implement-with-factory-and-reflection.md
index e9e11507..54307131 100644
--- a/docs/basics/java-basic/ioc-implement-with-factory-and-reflection.md
+++ b/docs/basics/java-basic/ioc-implement-with-factory-and-reflection.md
@@ -10,7 +10,7 @@
反射主要是指程序可以访问,检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。在Java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。
-反射是Java中一种强大的工具,能够使我们很方便的创建灵活的代码,这些代码可以再运行时装配,无需在组件之间进行源代码链接。但是反射使用不当会成本很高!类中有什么信息,利用反射机制就能可以获得什么信息,不过前提是得知道类的名字。
+反射是Java中一种强大的工具,能够使我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码链接。但是反射使用不当会成本很高!类中有什么信息,利用反射机制就能可以获得什么信息,不过前提是得知道类的名字。
### 反射机制的作用
@@ -24,13 +24,13 @@
静态编译:在编译时确定类型,绑定对象,即通过。
-动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了Java的灵活性,体现了多态的应用,有以降低类之间的藕合性。
+动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了Java的灵活性,体现了多态的应用,用以降低类之间的藕合性。
### 反射机制的优缺点
反射机制的优点:可以实现动态创建对象和编译,体现出很大的灵活性(特别是在J2EE的开发中它的灵活性就表现的十分明显)。通过反射机制我们可以获得类的各种内容,进行反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象。
-比如,一个大型的软件,不可能一次就把把它设计得很完美,把这个程序编译后,发布了,当发现需要更新某些功能时,我们不可能要用户把以前的卸载,再重新安装新的版本,假如这样的话,这个软件肯定是没有多少人用的。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用卸载,只需要在运行时动态地创建和编译,就可以实现该功能。
+比如,一个大型的软件,不可能一次就把它设计得很完美,把这个程序编译后,发布了,当发现需要更新某些功能时,我们不可能要用户把以前的卸载,再重新安装新的版本,假如这样的话,这个软件肯定是没有多少人用的。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用卸载,只需要在运行时动态地创建和编译,就可以实现该功能。
反射机制的缺点:对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且让它满足我们的要求。这类操作总是慢于直接执行相同的操作。
@@ -170,9 +170,9 @@ Spring中的IoC的实现原理就是工厂模式加反射机制。 我们首先
### IOC容器的技术剖析
-IOC中最基本的技术就是“反射(Reflection)”编程,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象,这种编程方式可以让对象在生成时才被决定到底是哪一种对象。只是在Spring中要生产的对象都在配置文件中给出定义,目的就是提高灵活性和可维护性。
+IOC中最基本的技术就是“反射(Reflection)”编程,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象,这种编程方式可以让对象在生成时才被决定到底是哪一种对象。只要是在Spring中生产的对象都要在配置文件中给出定义,目的就是提高灵活性和可维护性。
-目前C#、Java和PHP5等语言均支持反射,其中PHP5的技术书籍中,有时候也被翻译成“映射”。有关反射的概念和用法,大家应该都很清楚。反射的应用是很广泛的,很多的成熟的框架,比如像Java中的Hibernate、Spring框架,.Net中NHibernate、Spring.NET框架都是把”反射“做为最基本的技术手段。
+目前C#、Java和PHP5等语言均支持反射,其中PHP5的技术书籍中,有时候也被翻译成“映射”。有关反射的概念和用法,大家应该都很清楚。反射的应用是很广泛的,很多的成熟的框架,比如像Java中的Hibernate、Spring框架,.Net中NHibernate、Spring.NET框架都是把”反射“作为最基本的技术手段。
反射技术其实很早就出现了,但一直被忽略,没有被进一步的利用。当时的反射编程方式相对于正常的对象生成方式要慢至少得10倍。现在的反射技术经过改良优化,已经非常成熟,反射方式生成对象和通常对象生成方式,速度已经相差不大了,大约为1-2倍的差距。
@@ -188,8 +188,8 @@ IOC中最基本的技术就是“反射(Reflection)”编程,通俗来讲就
2)由于IOC容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。
- 3)具体到IOC框架产品(比如Spring)来讲,需要进行大量的配制工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
+ 3)具体到IOC框架产品(比如Spring)来讲,需要进行大量的配置工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
4)IOC框架产品本身的成熟度需要进行评估,如果引入一个不成熟的IOC框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。
-我们大体可以得出这样的结论:一些工作量不大的项目或者产品,不太适合使用IOC框架产品。另外,如果团队成员的知识能力欠缺,对于IOC框架产品缺乏深入的理解,也不要贸然引入。最后,特别强调运行效率的项目或者产品,也不太适合引入IOC框架产品,像WEB2.0网站就是这种情况。
\ No newline at end of file
+我们大体可以得出这样的结论:一些工作量不大的项目或者产品,不太适合使用IOC框架产品。另外,如果团队成员的知识能力欠缺,对于IOC框架产品缺乏深入的理解,也不要贸然引入。最后,特别强调运行效率的项目或者产品,也不太适合引入IOC框架产品,像WEB2.0网站就是这种情况。
diff --git a/docs/basics/java-basic/length-of-string.md b/docs/basics/java-basic/length-of-string.md
index 9ea59f2c..6b9d37a3 100644
--- a/docs/basics/java-basic/length-of-string.md
+++ b/docs/basics/java-basic/length-of-string.md
@@ -2,7 +2,7 @@
这次在之前那篇文章的基础上除了增加了一些验证过程外,还有些错误内容的修正。我这次在分析过程中会尝试对Jdk的编译过程进行debug,并且会参考一些JVM规范等全方面的介绍下这个知识点。
-因为这个问题涉及到Java的编译原理相关的知识,所以通过视频的方式讲解会更加容易理解一些,视频我上传到了B站:https://www.bilibili.com/video/BV1uK4y1t7H1/。
+因为这个问题涉及到Java的编译原理相关的知识,所以通过视频的方式讲解会更加容易理解一些,视频我上传到了B站:[【灵魂拷问】Java中的String到底有没有长度限制?](https://www.bilibili.com/video/BV1uK4y1t7H1/)
### String的长度限制
@@ -87,7 +87,7 @@ CONSTANT_Utf8_info 结构用于表示字符串常量的值:
代码中可以看出,当参数类型为String,并且长度大于等于65535的时候,就会导致编译失败。
-这个地方大家可以尝试着debug一下javac的编译过程(视频中有对java的编译过程进行debug的方法),也可以发现这个地方会报错。
+这个地方大家可以尝试着debug一下javac的编译过程,也可以发现这个地方会报错。
如果我们尝试以65534个字符定义字符串,则会发现可以正常编译。
@@ -124,7 +124,7 @@ int 是一个 32 位变量类型,取正数部分来算的话,他们最长可
得到的字符串长度就有10万,另外我之前在实际应用中遇到过这个问题。
-之前一次系统对接,需要传输高清图片,约定的传输方式是对方将图片转成BASE6编码,我们接收到之后再转成图片。
+之前一次系统对接,需要传输高清图片,约定的传输方式是对方将图片转成BASE64编码,我们接收到之后再转成图片。
在将BASE64编码后的内容赋值给字符串的时候就抛了异常。
@@ -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/linux-io.md b/docs/basics/java-basic/linux-io.md
index 79fc8a6c..051973b7 100644
--- a/docs/basics/java-basic/linux-io.md
+++ b/docs/basics/java-basic/linux-io.md
@@ -49,7 +49,7 @@ while(true){
而多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。
-另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
+另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态是通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
@@ -60,10 +60,10 @@ while(true){
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
### 异步IO模型
-异步IO模型是比较理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
+异步IO模型是比较理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要知道实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
-也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作。
+也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用IO函数进行实际的读写操作。
注意,异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO。
-前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
\ No newline at end of file
+前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
diff --git a/docs/basics/java-basic/meta-annotation.md b/docs/basics/java-basic/meta-annotation.md
index 8c0f5823..310eddde 100644
--- a/docs/basics/java-basic/meta-annotation.md
+++ b/docs/basics/java-basic/meta-annotation.md
@@ -11,4 +11,5 @@
@Retention
就是元注解。
-元注解有四个:@Target(表示该注解可以用于什么地方)、@Retention(表示再什么级别保存该注解信息)、@Documented(将此注解包含再javadoc中)、@Inherited(允许子类继承父类中的注解)。
\ No newline at end of file
+元注解有六个:@Target(表示该注解可以用于什么地方)、@Retention(表示再什么级别保存该注解信息)、@Documented(将此注解包含再javadoc中)、@Inherited(允许子类继承父类中的注解)、@Repeatable(1.8新增,允许一个注解在一个元素上使用多次)、@Native(1.8新增,修饰成员变量,表示这个变量可以被本地代码引用,常常被代码生成工具使用)。
+
diff --git a/docs/basics/java-basic/protobuf.md b/docs/basics/java-basic/protobuf.md
index f8c8738c..170b140d 100644
--- a/docs/basics/java-basic/protobuf.md
+++ b/docs/basics/java-basic/protobuf.md
@@ -3,7 +3,7 @@ Protocol Buffer (简称Protobuf) 是Google出品的性能优异、跨语言、
2001年初,Protobuf首先在Google内部创建, 我们把它称之为 proto1,一直以来在Google的内部使用,其中也不断的演化,根据使用者的需求也添加很多新的功能,一些内部库依赖它。几乎每个Google的开发者都会使用到它。
-Google开始开源它的内部项目时,因为依赖的关系,所以他们决定首先把Protobuf开源出去。
+Google开始开源它的内部项目时,因为依赖的关系,他们决定首先把Protobuf开源出去。
-目前Protobuf的稳定版本是3.9.2,于2019年9月23日发布。由于很多公司很早的就采用了Protobuf,所以很多项目还在使用proto2协议,目前是proto2和proto3同时在使用的状态。
+目前Protobuf的稳定版本是3.9.2,于2019年9月23日发布。由于很多公司很早的就采用了Protobuf,很多项目还在使用proto2协议,目前是proto2和proto3同时在使用的状态。
diff --git a/docs/basics/java-basic/replace-in-string.md b/docs/basics/java-basic/replace-in-string.md
index d89f5651..9c9b31cc 100644
--- a/docs/basics/java-basic/replace-in-string.md
+++ b/docs/basics/java-basic/replace-in-string.md
@@ -12,72 +12,17 @@ replaceAll和replaceFirst的区别主要是替换的内容不同,replaceAll是
### 用法例子
-一以下例子参考:http://www.51gjie.com/java/771.html
-
-1. replaceAll() 替换符合正则的所有文字
-
-```
-//文字替换(全部)
-Pattern pattern = Pattern.compile("正则表达式");
-Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World");
-//替换第一个符合正则的数据
-System.out.println(matcher.replaceAll("Java"));
-
-```
-
-
-2. replaceFirst() 替换第一个符合正则的数据
-
-```
-//文字替换(首次出现字符)
-Pattern pattern = Pattern.compile("正则表达式");
-Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World");
-//替换第一个符合正则的数据
-System.out.println(matcher.replaceFirst("Java"));
-
-```
-
-3. replaceAll()替换所有html标签
-
-```
-//去除html标记
-Pattern pattern = Pattern.compile("<.+?>", Pattern.DOTALL);
-Matcher matcher = pattern.matcher("主页");
-String string = matcher.replaceAll("");
-System.out.println(string);
-
-```
-
-4. replaceAll() 替换指定文字
-```
-//替换指定{}中文字
-String str = "Java目前的发展史是由{0}年-{1}年";
-String[][] object = {
- new String[] {
- "\\{0\\}",
- "1995"
- },
- new String[] {
- "\\{1\\}",
- "2007"
- }
-};
-System.out.println(replace(str, object));
-public static String replace(final String sourceString, Object[] object) {
- String temp = sourceString;
- for (int i = 0; i < object.length; i++) {
- String[] result = (String[]) object[i];
- Pattern pattern = Pattern.compile(result[0]);
- Matcher matcher = pattern.matcher(temp);
- temp = matcher.replaceAll(result[1]);
- }
- return temp;
-}
-
-```
-
-5. replace()替换字符串
-
-```
-System.out.println("abac".replace("a", "\\a")); //\ab\ac
-```
+ String string = "abc123adb23456aa";
+ System.out.println(string);//abc123adb23456aa
+
+ //使用replace将a替换成H
+ System.out.println(string.replace("a","H"));//Hbc123Hdb23456HH
+ //使用replaceFirst将第一个a替换成H
+ System.out.println(string.replaceFirst("a","H"));//Hbc123adb23456aa
+ //使用replace将a替换成H
+ System.out.println(string.replaceAll("a","H"));//Hbc123Hdb23456HH
+
+ //使用replaceFirst将第一个数字替换成H
+ System.out.println(string.replaceFirst("\\d","H"));//abcH23adb23456aa
+ //使用replaceAll将所有数字替换成H
+ System.out.println(string.replaceAll("\\d","H"));//abcHHHadbHHHHHaa
\ No newline at end of file
diff --git a/docs/basics/java-basic/scope.md b/docs/basics/java-basic/scope.md
deleted file mode 100644
index fb44d530..00000000
--- a/docs/basics/java-basic/scope.md
+++ /dev/null
@@ -1,7 +0,0 @@
-### 对于成员变量和方法的作用域,public,protected,private以及不写之间的区别。
-
-
-- public :表明该成员变量或者方法是对所有类或者对象都是可见的,所有类或者对象都可以直接访问
-- private:表明该成员变量或者方法是私有的,只有当前类对其具有访问权限,除此之外其他类或者对象都没有访问权限.子类也没有访问权限.
-- protected:表明成员变量或者方法对类自身,与同在一个包中的其他类可见,其他包下的类不可访问,除非是他的子类
-- default:表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类
\ No newline at end of file
diff --git a/docs/basics/java-basic/set-repetition.md b/docs/basics/java-basic/set-repetition.md
index e290e936..d9072a77 100644
--- a/docs/basics/java-basic/set-repetition.md
+++ b/docs/basics/java-basic/set-repetition.md
@@ -1,9 +1,9 @@
在Java的Set体系中,根据实现方式不同主要分为两大类。HashSet和TreeSet。
-1、TreeSet 是二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值
-2、HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束
+1、TreeSet 是二叉树实现的,TreeSet中的数据是自动排好序的,不允许放入 null值
+2、HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入 null值,但只能放入一个null,两者中的值都不能重复,就如数据库中的唯一约束
-在HashSet中,基本的操作都是有HashMap底层实现的,因为HashSet底层是用HashMap存储数据的。当向HashSet中添加元素的时候,首先计算元素的hashcode值,然后通过扰动计算和按位与的方式计算出这个元素的存储位置,如果这个位置位空,就将元素添加进去;如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找一个空位添加。
+在HashSet中,基本的操作都是由HashMap底层实现的,因为HashSet底层是用HashMap存储数据的。当向HashSet中添加元素的时候,首先计算元素的hashCode值,然后通过扰动计算和按位与的方式计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找一个空位添加。
TreeSet的底层是TreeMap的keySet(),而TreeMap是基于红黑树实现的,红黑树是一种平衡二叉查找树,它能保证任何一个节点的左右子树的高度差不会超过较矮的那棵的一倍。
diff --git a/docs/basics/java-basic/static-proxy.md b/docs/basics/java-basic/static-proxy.md
index 7886da15..b3c474e4 100644
--- a/docs/basics/java-basic/static-proxy.md
+++ b/docs/basics/java-basic/static-proxy.md
@@ -12,7 +12,7 @@ public class HelloSeriviceImpl implements HelloSerivice{
}
}
```
-上面的代码比较简单,定义了一个接口和其实现类。这就是代理模式中的目标对象和目标对象的接口。接下类定义代理对象。
+上面的代码比较简单,定义了一个接口和其实现类。这就是代理模式中的目标对象和目标对象的接口。接下来定义代理对象。
```
public class HelloSeriviceProxy implements HelloSerivice{
@@ -50,9 +50,10 @@ public class Main {
这就是一个简单的静态的代理模式的实现。代理模式中的所有角色(代理对象、目标对象、目标对象的接口)等都是在编译期就确定好的。
-静态代理的用途
-控制真实对象的访问权限 通过代理对象控制对真实对象的使用权限。
+静态代理的用途
-避免创建大对象 通过使用一个代理小对象来代表一个真实的大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
+1.控制真实对象的访问权限:通过代理对象控制真实对象的使用权限。
-增强真实对象的功能 这个比较简单,通过代理可以在调用真实对象的方法的前后增加额外功能。
\ No newline at end of file
+2.避免创建大对象:通过使用一个代理小对象来代表一个真实的大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
+
+3.增强真实对象的功能:这个比较简单,通过代理可以在调用真实对象的方法的前后增加额外功能。
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/stop-use-enum-in-api.md b/docs/basics/java-basic/stop-use-enum-in-api.md
new file mode 100644
index 00000000..95f54aff
--- /dev/null
+++ b/docs/basics/java-basic/stop-use-enum-in-api.md
@@ -0,0 +1,175 @@
+最近,我们的线上环境出现了一个问题,线上代码在执行过程中抛出了一个IllegalArgumentException,分析堆栈后,发现最根本的的异常是以下内容:
+
+ java.lang.IllegalArgumentException:
+ No enum constant com.a.b.f.m.a.c.AType.P_M
+
+
+大概就是以上的内容,看起来还是很简单的,提示的错误信息就是在AType这个枚举类中没有找到P_M这个枚举项。
+
+于是经过排查,我们发现,在线上开始有这个异常之前,该应用依赖的一个下游系统有发布,而发布过程中是一个API包发生了变化,主要变化内容是在一个RPC接口的Response返回值类中的一个枚举参数AType中增加了P_M这个枚举项。
+
+但是下游系统发布时,并未通知到我们负责的这个系统进行升级,所以就报错了。
+
+我们来分析下为什么会发生这样的情况。
+
+### 问题重现
+
+首先,下游系统A提供了一个二方库的某一个接口的返回值中有一个参数类型是枚举类型。
+
+> 一方库指的是本项目中的依赖 二方库指的是公司内部其他项目提供的依赖 三方库指的是其他组织、公司等来自第三方的依赖
+
+ public interface AFacadeService {
+
+ public AResponse doSth(ARequest aRequest);
+ }
+
+ public Class AResponse{
+
+ private Boolean success;
+
+ private AType aType;
+ }
+
+ public enum AType{
+
+ P_T,
+
+ A_B
+ }
+
+
+然后B系统依赖了这个二方库,并且会通过RPC远程调用的方式调用AFacadeService的doSth方法。
+
+ public class BService {
+
+ @Autowired
+ AFacadeService aFacadeService;
+
+ public void doSth(){
+ ARequest aRequest = new ARequest();
+
+ AResponse aResponse = aFacadeService.doSth(aRequest);
+
+ AType aType = aResponse.getAType();
+ }
+ }
+
+
+这时候,如果A和B系统依赖的都是同一个二方库的话,两者使用到的枚举AType会是同一个类,里面的枚举项也都是一致的,这种情况不会有什么问题。
+
+但是,如果有一天,这个二方库做了升级,在AType这个枚举类中增加了一个新的枚举项P_M,这时候只有系统A做了升级,但是系统B并没有做升级。
+
+那么A系统依赖的的AType就是这样的:
+
+ public enum AType{
+
+ P_T,
+
+ A_B,
+
+ P_M
+ }
+
+
+而B系统依赖的AType则是这样的:
+
+ public enum AType{
+
+ P_T,
+
+ A_B
+ }
+
+
+这种情况下**,在B系统通过RPC调用A系统的时候,如果A系统返回的AResponse中的aType的类型位新增的P_M时候,B系统就会无法解析。一般在这种时候,RPC框架就会发生反序列化异常。导致程序被中断。**
+
+### 原理分析
+
+这个问题的现象我们分析清楚了,那么再来看下原理是怎样的,为什么出现这样的异常呢。
+
+其实这个原理也不难,这类**RPC框架大多数会采用JSON的格式进行数据传输**,也就是客户端会将返回值序列化成JSON字符串,而服务端会再将JSON字符串反序列化成一个Java对象。
+
+而JSON在反序列化的过程中,对于一个枚举类型,会尝试调用对应的枚举类的valueOf方法来获取到对应的枚举。
+
+而我们查看枚举类的valueOf方法的实现时,就可以发现,**如果从枚举类中找不到对应的枚举项的时候,就会抛出IllegalArgumentException**:
+
+ public static > T valueOf(Class enumType,
+ String name) {
+ T result = enumType.enumConstantDirectory().get(name);
+ if (result != null)
+ return result;
+ if (name == null)
+ throw new NullPointerException("Name is null");
+ throw new IllegalArgumentException(
+ "No enum constant " + enumType.getCanonicalName() + "." + name);
+ }
+
+
+关于这个问题,其实在《阿里巴巴Java开发手册》中也有类似的约定:
+
+![-w1538][1]
+
+这里面规定"**对于二方库的参数可以使用枚举,但是返回值不允许使用枚举**"。这背后的思考就是本文上面提到的内容。
+
+### 扩展思考
+
+**为什么参数中可以有枚举?**
+
+不知道大家有没有想过这个问题,其实这个就和二方库的职责有点关系了。
+
+一般情况下,A系统想要提供一个远程接口给别人调用的时候,就会定义一个二方库,告诉其调用方如何构造参数,调用哪个接口。
+
+而这个二方库的调用方会根据其中定义的内容来进行调用。而参数的构造过程是由B系统完成的,如果B系统使用到的是一个旧的二方库,使用到的枚举自然是已有的一些,新增的就不会被用到,所以这样也不会出现问题。
+
+比如前面的例子,B系统在调用A系统的时候,构造参数的时候使用到AType的时候就只有P_T和A_B两个选项,虽然A系统已经支持P_M了,但是B系统并没有使用到。
+
+如果B系统想要使用P_M,那么就需要对该二方库进行升级。
+
+但是,返回值就不一样了,返回值并不受客户端控制,服务端返回什么内容是根据他自己依赖的二方库决定的。
+
+但是,其实相比较于手册中的规定,**我更加倾向于,在RPC的接口中入参和出参都不要使用枚举。**
+
+一般,我们要使用枚举都是有几个考虑:
+
+* 1、枚举严格控制下游系统的传入内容,避免非法字符。
+
+* 2、方便下游系统知道都可以传哪些值,不容易出错。
+
+不可否认,使用枚举确实有一些好处,但是我不建议使用主要有以下原因:
+
+* 1、如果二方库升级,并且删除了一个枚举中的部分枚举项,那么入参中使用枚举也会出现问题,调用方将无法识别该枚举项。
+
+* 2、有的时候,上下游系统有多个,如C系统通过B系统间接调用A系统,A系统的参数是由C系统传过来的,B系统只是做了一个参数的转换与组装。这种情况下,一旦A系统的二方库升级,那么B和C都要同时升级,任何一个不升级都将无法兼容。
+
+**我其实建议大家在接口中使用字符串代替枚举**,相比较于枚举这种强类型,字符串算是一种弱类型。
+
+如果使用字符串代替RPC接口中的枚举,那么就可以避免上面我们提到的两个问题,上游系统只需要传递字符串就行了,而具体的值的合法性,只需要在A系统内自己进行校验就可以了。
+
+**为了方便调用者使用,可以使用javadoc的@see注解表明这个字符串字段的取值从那个枚举中获取。**
+
+ public Class AResponse{
+
+ private Boolean success;
+
+ /**
+ * @see AType
+ */
+ private String aType;
+ }
+
+
+对于像阿里这种比较庞大的互联网公司,**随便提供出去的一个接口,可能有上百个调用方**,而接口升级也是常态,**我们根本做不到每次二方库升级之后要求所有调用者跟着一起升级**,这是完全不现实的,并且对于有些调用者来说,他用不到新特性,完全没必要做升级。
+
+还有一种看起来比较特殊,但是实际上比较常见的情况,就是有的时候一个接口的声明在A包中,而一些枚举常量定义在B包中,比较常见的就是阿里的交易相关的信息,订单分很多层次,每次引入一个包的同时都需要引入几十个包。
+
+对于调用者来说,我肯定是不希望我的系统引入太多的依赖的,**一方面依赖多了会导致应用的编译过程很慢,并且很容易出现依赖冲突问题。**
+
+所以,在调用下游接口的时候,如果参数中字段的类型是枚举的话,那我没办法,必须得依赖他的二方库。但是如果不是枚举,只是一个字符串,那我就可以选择不依赖。
+
+所以,我们在定义接口的时候,会尽量避免使用枚举这种强类型。规范中规定在返回值中不允许使用,而我自己要求更高,就是即使在接口的入参中我也很少使用。
+
+最后,我只是不建议在对外提供的接口的出入参中使用枚举,并不是说彻底不要用枚举,我之前很多文章也提到过,枚举有很多好处,我在代码中也经常使用。所以,切不可因噎废食。
+
+当然,文中的观点仅代表我个人,具体是是不是适用其他人,其他场景或者其他公司的实践,需要读者们自行分辨下,建议大家在使用的时候可以多思考一下。
+
+ [1]: https://www.hollischuang.com/wp-content/uploads/2020/11/16066271055035-scaled.jpg
\ No newline at end of file
diff --git a/docs/basics/java-basic/stop-using-equlas-in-bigdecimal.md b/docs/basics/java-basic/stop-using-equlas-in-bigdecimal.md
new file mode 100644
index 00000000..15895f69
--- /dev/null
+++ b/docs/basics/java-basic/stop-using-equlas-in-bigdecimal.md
@@ -0,0 +1,174 @@
+BigDecimal,相信对于很多人来说都不陌生,很多人都知道他的用法,这是一种java.math包中提供的一种可以用来进行精确运算的类型。
+
+很多人都知道,在进行金额表示、金额计算等场景,不能使用double、float等类型,而是要使用对精度支持的更好的BigDecimal。
+
+所以,很多支付、电商、金融等业务中,BigDecimal的使用非常频繁。而且不得不说这是一个非常好用的类,其内部自带了很多方法,如加,减,乘,除等运算方法都是可以直接调用的。
+
+除了需要用BigDecimal表示数字和进行数字运算以外,代码中还经常需要对于数字进行相等判断。
+
+关于这个知识点,在最新版的《阿里巴巴Java开发手册》中也有说明:
+
+![][1]
+
+这背后的思考是什么呢?
+
+我在之前的CodeReview中,看到过以下这样的低级错误:
+
+ if(bigDecimal == bigDecimal1){
+ // 两个数相等
+ }
+
+
+这种错误,相信聪明的读者一眼就可以看出问题,**因为BigDecimal是对象,所以不能用`==`来判断两个数字的值是否相等。**
+
+以上这种问题,在有一定的经验之后,还是可以避免的,但是聪明的读者,看一下以下这行代码,你觉得他有问题吗:
+
+ if(bigDecimal.equals(bigDecimal1)){
+ // 两个数相等
+ }
+
+
+可以明确的告诉大家,以上这种写法,可能得到的结果和你预想的不一样!
+
+先来做个实验,运行以下代码:
+
+ BigDecimal bigDecimal = new BigDecimal(1);
+ BigDecimal bigDecimal1 = new BigDecimal(1);
+ System.out.println(bigDecimal.equals(bigDecimal1));
+
+
+ BigDecimal bigDecimal2 = new BigDecimal(1);
+ BigDecimal bigDecimal3 = new BigDecimal(1.0);
+ System.out.println(bigDecimal2.equals(bigDecimal3));
+
+
+ BigDecimal bigDecimal4 = new BigDecimal("1");
+ BigDecimal bigDecimal5 = new BigDecimal("1.0");
+ System.out.println(bigDecimal4.equals(bigDecimal5));
+
+
+以上代码,输出结果为:
+
+ true
+ true
+ false
+
+
+### BigDecimal的equals原理
+
+通过以上代码示例,我们发现,在使用BigDecimal的equals方法对1和1.0进行比较的时候,有的时候是true(当使用int、double定义BigDecimal时),有的时候是false(当使用String定义BigDecimal时)。
+
+那么,为什么会出现这样的情况呢,我们先来看下BigDecimal的equals方法。
+
+在BigDecimal的JavaDoc中其实已经解释了其中原因:
+
+ Compares this BigDecimal with the specified Object for equality. Unlike compareTo, this method considers two BigDecimal objects equal only if they are equal in value and scale (thus 2.0 is not equal to 2.00 when compared by this method)
+
+
+大概意思就是,**equals方法和compareTo并不一样,equals方法会比较两部分内容,分别是值(value)和标度(scale)**
+
+
+对应的代码如下:
+
+![][2]
+
+所以,我们以上代码定义出来的两个BigDecimal对象(bigDecimal4和bigDecimal5)的标度是不一样的,所以使用equals比较的结果就是false了。
+
+尝试着对代码进行debug,在debug的过程中我们也可以看到bigDecimal4的标度时0,而bigDecimal5的标度是1。
+
+![][3]
+
+到这里,我们大概解释清楚了,之所以equals比较bigDecimal4和bigDecimal5的结果是false,是因为标度不同。
+
+那么,为什么标度不同呢?为什么bigDecimal2和bigDecimal3的标度是一样的(当使用int、double定义BigDecimal时),而bigDecimal4和bigDecimal5却不一样(当使用String定义BigDecimal时)呢?
+
+### 为什么标度不同
+
+这个就涉及到BigDecimal的标度问题了,这个问题其实是比较复杂的,由于不是本文的重点,这里面就简单介绍一下吧。大家感兴趣的话,后面单独讲。
+
+首先,BigDecimal一共有以下4个构造方法:
+
+ BigDecimal(int)
+ BigDecimal(double)
+ BigDecimal(long)
+ BigDecimal(String)
+
+
+以上四个方法,创建出来的的BigDecimal的标度是不同的。
+
+#### BigDecimal(long) 和BigDecimal(int)
+
+首先,最简单的就是**BigDecimal(long) 和BigDecimal(int),因为是整数,所以标度就是0** :
+
+ public BigDecimal(int val) {
+ this.intCompact = val;
+ this.scale = 0;
+ this.intVal = null;
+ }
+
+ public BigDecimal(long val) {
+ this.intCompact = val;
+ this.intVal = (val == INFLATED) ? INFLATED_BIGINT : null;
+ this.scale = 0;
+ }
+
+
+#### BigDecimal(double)
+
+而对于BigDecimal(double) ,**当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是整好等于0.1的,而是0.1000000000000000055511151231257827021181583404541015625 。这是因为doule自身表示的只是一个近似值。**
+
+那么,无论我们使用new BigDecimal(0.1)还是new BigDecimal(0.10)定义,他的近似值都是0.1000000000000000055511151231257827021181583404541015625这个,那么他的标度就是这个数字的位数,即55。
+
+![][4]
+
+其他的浮点数也同样的道理。对于new BigDecimal(1.0)这样的形式来说,因为他本质上也是个整数,所以他创建出来的数字的标度就是0。
+
+所以,因为BigDecimal(1.0)和BigDecimal(1.00)的标度是一样的,所以在使用equals方法比较的时候,得到的结果就是true。
+
+#### BigDecimal(string)
+
+而对于BigDecimal(double) ,**当我们使用new BigDecimal("0.1")创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。那么他的标度也就是1。**
+
+如果使用new BigDecimal("0.10000"),那么创建出来的数就是0.10000,标度也就是5。
+
+所以,因为BigDecimal("1.0")和BigDecimal("1.00")的标度不一样,所以在使用equals方法比较的时候,得到的结果就是false。
+
+### 如何比较BigDecimal
+
+前面,我们解释了BigDecimal的equals方法,其实不只是会比较数字的值,还会对其标度进行比较。
+
+所以,当我们使用equals方法判断判断两个数是否相等的时候,是极其严格的。
+
+那么,如果我们只想判断两个BigDecimal的值是否相等,那么该如何判断呢?
+
+**BigDecimal中提供了compareTo方法,这个方法就可以只比较两个数字的值,如果两个数相等,则返回0。**
+
+ BigDecimal bigDecimal4 = new BigDecimal("1");
+ BigDecimal bigDecimal5 = new BigDecimal("1.0000");
+ System.out.println(bigDecimal4.compareTo(bigDecimal5));
+
+
+以上代码,输出结果:
+
+ 0
+
+
+其源码如下:
+
+![][5]
+
+### 总结
+
+BigDecimal是一个非常好用的表示高精度数字的类,其中提供了很多丰富的方法。
+
+但是,他的equals方法使用的时候需要谨慎,因为他在比较的时候,不仅比较两个数字的值,还会比较他们的标度,只要这两个因素有一个是不相等的,那么结果也是false、
+
+如果读者想要对两个BigDecimal的数值进行比较的话,可以使用compareTo方法。
+
+
+ [1]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004945569932.jpg
+ [2]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004955317132.jpg
+ [3]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004956382289.jpg
+ [4]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004965161081.jpg
+ [5]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004972460075.jpg
+ [6]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004976158870.jpg
\ No newline at end of file
diff --git a/docs/basics/java-basic/stream.md b/docs/basics/java-basic/stream.md
index 9455b4df..1f95713f 100644
--- a/docs/basics/java-basic/stream.md
+++ b/docs/basics/java-basic/stream.md
@@ -66,7 +66,7 @@ filter 方法用于通过设置的条件过滤出元素。以下代码片段使
List strings = Arrays.asList("Hollis", "", "HollisChuang", "H", "hollis");
strings.stream().filter(string -> !string.isEmpty()).forEach(System.out::println);
- //Hollis, , HollisChuang, H, hollis
+ //Hollis, HollisChuang, H, hollis
**map**
@@ -74,13 +74,13 @@ filter 方法用于通过设置的条件过滤出元素。以下代码片段使
map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:
List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
- numbers.stream().map( i -> i*i).forEach(System.out::println);
+ numbers.stream().map(i -> i*i).forEach(System.out::println);
//9,4,4,9,49,9,25
**limit/skip**
-limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。以下代码片段使用 limit 方法保理4个元素:
+limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。以下代码片段使用 limit 方法保留4个元素:
List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().limit(4).forEach(System.out::println);
@@ -120,9 +120,9 @@ distinct主要用来去重,以下代码片段使用 distinct 对元素进行
### Stream最终操作
-Stream的中间操作得到的结果还是一个Stream,那么如何把一个Stream转换成我们需要的类型呢?比如计算出流中元素的个数、将流装换成集合等。这就需要最终操作(terminal operation)
+Stream的中间操作得到的结果还是一个Stream,那么如何把一个Stream转换成我们需要的类型呢?比如计算出流中元素的个数、将流转换成集合等。这就需要最终操作(terminal operation)
-最终操作会消耗流,产生一个最终结果。也就是说,在最终操作之后,不能再次使用流,也不能在使用任何中间操作,否则将抛出异常:
+最终操作会消耗流,产生一个最终结果。也就是说,在最终操作之后,不能再次使用流,也不能再使用任何中间操作,否则将抛出异常:
java.lang.IllegalStateException: stream has already been operated upon or closed
@@ -145,7 +145,7 @@ Stream 提供了方法 'forEach' 来迭代流中的每个数据。以下代码
count用来统计流中的元素个数。
- List strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis");
+ List strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hollis666", "Hello", "HelloWorld", "Hollis");
System.out.println(strings.stream().count());
//7
@@ -160,23 +160,23 @@ collect就是一个归约操作,可以接受各种做法作为参数,将流
//Hollis, HollisChuang, Hollis666, Hollis
-接下来,我们还是使用一张图,来演示下,前文的例子中,当一个Stream先后通过filter、map、sort、limit以及distinct处理后会,在分别使用不同的最终操作可以得到怎样的结果:
+接下来,我们还是使用一张图,来演示下,前文的例子中,当一个Stream先后通过filter、map、sort、limit以及distinct处理后,在分别使用不同的最终操作可以得到怎样的结果:
下图,展示了文中介绍的所有操作的位置、输入、输出以及使用一个案例展示了其结果。 ![][6]
### 总结
-本文介绍了Java 8中的Stream 的用途,优点等。还接受了Stream的几种用法,分别是Stream创建、中间操作和最终操作。
+本文介绍了Java 8中的Stream 的用途,优点等。还介绍了Stream的几种用法,分别是Stream创建、中间操作和最终操作。
Stream的创建有两种方式,分别是通过集合类的stream方法、通过Stream的of方法。
Stream的中间操作可以用来处理Stream,中间操作的输入和输出都是Stream,中间操作可以是过滤、转换、排序等。
-Stream的最终操作可以将Stream转成其他形式,如计算出流中元素的个数、将流装换成集合、以及元素的遍历等。
+Stream的最终操作可以将Stream转成其他形式,如计算出流中元素的个数、将流转换成集合、以及元素的遍历等。
[1]: https://www.hollischuang.com/wp-content/uploads/2019/03/15521192454583.jpg
[2]: https://www.hollischuang.com/wp-content/uploads/2019/03/15521194075219.jpg
[3]: https://www.hollischuang.com/wp-content/uploads/2019/03/15521194556484.jpg
[4]: https://www.hollischuang.com/wp-content/uploads/2019/03/15521242025506.jpg
[5]: https://www.hollischuang.com/wp-content/uploads/2019/03/15521194606851.jpg
- [6]: https://www.hollischuang.com/wp-content/uploads/2019/03/15521245463720.jpg
\ No newline at end of file
+ [6]: https://www.hollischuang.com/wp-content/uploads/2019/03/15521245463720.jpg
diff --git a/docs/basics/java-basic/string-append.md b/docs/basics/java-basic/string-append.md
index 6d908651..eae5eb66 100644
--- a/docs/basics/java-basic/string-append.md
+++ b/docs/basics/java-basic/string-append.md
@@ -1,3 +1,31 @@
-1. String s = "a" + "b",编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),即变成 String s = "ab"
+Java中,想要拼接字符串,最简单的方式就是通过"+"连接两个字符串。
-2. 对于能够进行优化的(String s = "a" + 变量 等)用 StringBuilder 的 append() 方法替代,最后调用 toString() 方法 (底层就是一个 new String())
\ No newline at end of file
+有人把Java中使用+拼接字符串的功能理解为运算符重载。其实并不是,Java是不支持运算符重载的。这其实只是Java提供的一个语法糖。
+
+>运算符重载:在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
+
+>语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
+
+前面提到过,使用+拼接字符串,其实只是Java提供的一个语法糖, 那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。
+
+还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。
+
+ String wechat = "Hollis";
+ String introduce = "每日更新Java相关技术文章";
+ String hollis = wechat + "," + introduce;
+
+反编译后的内容如下,反编译工具为jad。
+
+ String wechat = "Hollis";
+ String introduce = "\u6BCF\u65E5\u66F4\u65B0Java\u76F8\u5173\u6280\u672F\u6587\u7AE0";//每日更新Java相关技术文章
+ String hollis = (new StringBuilder()).append(wechat).append(",").append(introduce).toString();
+
+通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。
+
+那么也就是说,Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append。
+
+但是,String的使用+字符串拼接也不全都是基于StringBuilder.append,还有种特殊情况,那就是如果是两个固定的字面量拼接,如:
+
+ String s = "a" + "b"
+
+编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),直接变成 String s = "ab"。
diff --git a/docs/basics/java-basic/string-concat.md b/docs/basics/java-basic/string-concat.md
index 9fa634ff..0b7f73d5 100644
--- a/docs/basics/java-basic/string-concat.md
+++ b/docs/basics/java-basic/string-concat.md
@@ -16,9 +16,8 @@
其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码:
-String s = "abcd";
-s = s.concat("ef");
-
+ String s = "abcd";
+ s = s.concat("ef");
其实最后我们得到的s已经是一个新的字符串了。如下图
@@ -32,24 +31,17 @@ s中保存的是一个重新创建出来的String对象的引用。
在Java中,拼接字符串最简单的方式就是直接使用符号`+`来拼接。如:
-String wechat = "Hollis";
-String introduce = "每日更新Java相关技术文章";
-String hollis = wechat + "," + introduce;
-
-
-这里要特别说明一点,有人把Java中使用`+`拼接字符串的功能理解为**运算符重载**。其实并不是,**Java是不支持运算符重载的**。这其实只是Java提供的一个**语法糖**。后面再详细介绍。
-
-> 运算符重载:在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
->
-> 语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
+ String wechat = "Hollis";
+ String introduce = "每日更新Java相关技术文章";
+ String hollis = wechat + "," + introduce;
**concat**
除了使用`+`拼接字符串之外,还可以使用String类中的方法concat方法来拼接字符串。如:
-String wechat = "Hollis";
-String introduce = "每日更新Java相关技术文章";
-String hollis = wechat.concat(",").concat(introduce);
-
+ String wechat = "Hollis";
+ String introduce = "每日更新Java相关技术文章";
+ String hollis = wechat.concat(",").concat(introduce);
+
**StringBuffer**
@@ -57,26 +49,25 @@ String hollis = wechat.concat(",").concat(introduce);
使用`StringBuffer`可以方便的对字符串进行拼接。如:
-StringBuffer wechat = new StringBuffer("Hollis");
-String introduce = "每日更新Java相关技术文章";
-StringBuffer hollis = wechat.append(",").append(introduce);
-
+ StringBuffer wechat = new StringBuffer("Hollis");
+ String introduce = "每日更新Java相关技术文章";
+ StringBuffer hollis = wechat.append(",").append(introduce);
+
**StringBuilder**
除了`StringBuffer`以外,还有一个类`StringBuilder`也可以使用,其用法和`StringBuffer`类似。如:
-StringBuilder wechat = new StringBuilder("Hollis");
-String introduce = "每日更新Java相关技术文章";
-StringBuilder hollis = wechat.append(",").append(introduce);
-
+ StringBuilder wechat = new StringBuilder("Hollis");
+ String introduce = "每日更新Java相关技术文章";
+ StringBuilder hollis = wechat.append(",").append(introduce);
**StringUtils.join**
除了JDK中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如`apache.commons中`提供的`StringUtils`类,其中的`join`方法可以拼接字符串。
-String wechat = "Hollis";
-String introduce = "每日更新Java相关技术文章";
-System.out.println(StringUtils.join(wechat, ",", introduce));
-
+ String wechat = "Hollis";
+ String introduce = "每日更新Java相关技术文章";
+ System.out.println(StringUtils.join(wechat, ",", introduce));
+
这里简单说一下,StringUtils中提供的join方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如:
@@ -96,41 +87,23 @@ System.out.println(StringUtils.join(wechat, ",", introduce));
### 使用`+`拼接字符串的实现原理
-前面提到过,使用`+`拼接字符串,其实只是Java提供的一个语法糖, 那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。
-
-还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。
-
-String wechat = "Hollis";
-String introduce = "每日更新Java相关技术文章";
-String hollis = wechat + "," + introduce;
-
-
-反编译后的内容如下,反编译工具为jad。
-
-String wechat = "Hollis";
-String introduce = "\u6BCF\u65E5\u66F4\u65B0Java\u76F8\u5173\u6280\u672F\u6587\u7AE0";//每日更新Java相关技术文章
-String hollis = (new StringBuilder()).append(wechat).append(",").append(introduce).toString();
-
-
-通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。
-
-那么也就是说,Java中的`+`对字符串的拼接,其实现原理是使用`StringBuilder.append`。
+关于这个知识点,前面的章节介绍过,主要是通过StringBuilder的append方法实现的。
### concat是如何实现的
我们再来看一下concat方法的源代码,看一下这个方法又是如何实现的。
-public String concat(String str) {
- int otherLen = str.length();
- if (otherLen == 0) {
- return this;
+ public String concat(String str) {
+ int otherLen = str.length();
+ if (otherLen == 0) {
+ return this;
+ }
+ int len = value.length;
+ char buf[] = Arrays.copyOf(value, len + otherLen);
+ str.getChars(buf, len);
+ return new String(buf, true);
}
- int len = value.length;
- char buf[] = Arrays.copyOf(value, len + otherLen);
- str.getChars(buf, len);
- return new String(buf, true);
-}
-
+
这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的String对象并返回。
@@ -142,45 +115,45 @@ String hollis = (new StringBuilder()).append(wechat).append(",").append(introduc
和`String`类类似,`StringBuilder`类也封装了一个字符数组,定义如下:
-char[] value;
-
+ char[] value;
+
与`String`不同的是,它并不是`final`的,所以他是可以修改的。另外,与`String`不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:
-int count;
-
+ int count;
+
其append源码如下:
-public StringBuilder append(String str) {
- super.append(str);
- return this;
-}
-
+ public StringBuilder append(String str) {
+ super.append(str);
+ return this;
+ }
+
该类继承了`AbstractStringBuilder`类,看下其`append`方法:
-public AbstractStringBuilder append(String str) {
- if (str == null)
- return appendNull();
- int len = str.length();
- ensureCapacityInternal(count + len);
- str.getChars(0, len, value, count);
- count += len;
- return this;
-}
-
+ public AbstractStringBuilder append(String str) {
+ if (str == null)
+ return appendNull();
+ int len = str.length();
+ ensureCapacityInternal(count + len);
+ str.getChars(0, len, value, count);
+ count += len;
+ return this;
+ }
+
append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。
`StringBuffer`和`StringBuilder`类似,最大的区别就是`StringBuffer`是线程安全的,看一下`StringBuffer`的`append`方法。
-public synchronized StringBuffer append(String str) {
- toStringCache = null;
- super.append(str);
- return this;
-}
-
+ public synchronized StringBuffer append(String str) {
+ toStringCache = null;
+ super.append(str);
+ return this;
+ }
+
该方法使用`synchronized`进行声明,说明是一个线程安全的方法。而`StringBuilder`则不是线程安全的。
@@ -188,56 +161,56 @@ append会直接拷贝字符到内部的字符数组中,如果字符数组长
通过查看`StringUtils.join`的源代码,我们可以发现,其实他也是通过`StringBuilder`来实现的。
-public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
- if (array == null) {
- return null;
- }
- if (separator == null) {
- separator = EMPTY;
- }
-
- // endIndex - startIndex > 0: Len = NofStrings *(len(firstString) + len(separator))
- // (Assuming that all Strings are roughly equally long)
- final int noOfItems = endIndex - startIndex;
- if (noOfItems <= 0) {
- return EMPTY;
- }
-
- final StringBuilder buf = new StringBuilder(noOfItems * 16);
-
- for (int i = startIndex; i < endIndex; i++) {
- if (i > startIndex) {
- buf.append(separator);
+ public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
+ if (array == null) {
+ return null;
+ }
+ if (separator == null) {
+ separator = EMPTY;
+ }
+
+ // endIndex - startIndex > 0: Len = NofStrings *(len(firstString) + len(separator))
+ // (Assuming that all Strings are roughly equally long)
+ final int noOfItems = endIndex - startIndex;
+ if (noOfItems <= 0) {
+ return EMPTY;
}
- if (array[i] != null) {
- buf.append(array[i]);
+
+ final StringBuilder buf = new StringBuilder(noOfItems * 16);
+
+ for (int i = startIndex; i < endIndex; i++) {
+ if (i > startIndex) {
+ buf.append(separator);
+ }
+ if (array[i] != null) {
+ buf.append(array[i]);
+ }
}
+ return buf.toString();
}
- return buf.toString();
-}
-
+
### 效率比较
既然有这么多种字符串拼接的方法,那么到底哪一种效率最高呢?我们来简单对比一下。
-long t1 = System.currentTimeMillis();
-//这里是初始字符串定义
-for (int i = 0; i < 50000; i++) {
- //这里是字符串拼接代码
-}
-long t2 = System.currentTimeMillis();
-System.out.println("cost:" + (t2 - t1));
-
+ long t1 = System.currentTimeMillis();
+ //这里是初始字符串定义
+ for (int i = 0; i < 50000; i++) {
+ //这里是字符串拼接代码
+ }
+ long t2 = System.currentTimeMillis();
+ System.out.println("cost:" + (t2 - t1));
+
我们使用形如以上形式的代码,分别测试下五种字符串拼接代码的运行时间。得到结果如下:
-+ cost:5119
-StringBuilder cost:3
-StringBuffer cost:4
-concat cost:3623
-StringUtils.join cost:25726
-
+ + cost:5119
+ StringBuilder cost:3
+ StringBuffer cost:4
+ concat cost:3623
+ StringUtils.join cost:25726
+
从结果可以看出,用时从短到长的对比是:
@@ -251,29 +224,29 @@ StringUtils.join也是使用了StringBuilder,并且其中还是有很多其他
我们再把以下代码反编译下:
-long t1 = System.currentTimeMillis();
-String str = "hollis";
-for (int i = 0; i < 50000; i++) {
- String s = String.valueOf(i);
- str += s;
-}
-long t2 = System.currentTimeMillis();
-System.out.println("+ cost:" + (t2 - t1));
-
+ long t1 = System.currentTimeMillis();
+ String str = "hollis";
+ for (int i = 0; i < 50000; i++) {
+ String s = String.valueOf(i);
+ str += s;
+ }
+ long t2 = System.currentTimeMillis();
+ System.out.println("+ cost:" + (t2 - t1));
+
反编译后代码如下:
-long t1 = System.currentTimeMillis();
-String str = "hollis";
-for(int i = 0; i < 50000; i++)
-{
- String s = String.valueOf(i);
- str = (new StringBuilder()).append(str).append(s).toString();
-}
-
-long t2 = System.currentTimeMillis();
-System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());
-
+ long t1 = System.currentTimeMillis();
+ String str = "hollis";
+ for(int i = 0; i < 50000; i++)
+ {
+ String s = String.valueOf(i);
+ str = (new StringBuilder()).append(str).append(s).toString();
+ }
+
+ long t2 = System.currentTimeMillis();
+ System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());
+
我们可以看到,反编译后的代码,在`for`循环中,每次都是`new`了一个`StringBuilder`,然后再把`String`转成`StringBuilder`,再进行`append`。
diff --git a/docs/basics/java-basic/stringjoiner-in-java8.md b/docs/basics/java-basic/stringjoiner-in-java8.md
new file mode 100644
index 00000000..36b82cbe
--- /dev/null
+++ b/docs/basics/java-basic/stringjoiner-in-java8.md
@@ -0,0 +1,158 @@
+在上一节中,我们介绍了几种Java中字符串拼接的方式,以及优缺点。其中还有一个重要的拼接方式我没有介绍,那就是Java 8中提供的StringJoiner ,本文就来介绍一下这个字符串拼接的新兵。
+
+如果你想知道一共有多少种方法可以进行字符串拼接,教你一个简单的办法,在Intellij IDEA中,定义一个Java Bean,然后尝试使用快捷键自动生成一个toString方法,IDEA会提示多种toString生成策略可供选择。
+
+![][2]
+
+目前我使用的IDEA的toString生成策略默认的是使用JDK 1.8提供的StringJoiner。
+
+### 介绍
+
+StringJoiner是java.util包中的一个类,用于构造一个由分隔符分隔的字符序列(可选),并且可以从提供的前缀开始并以提供的后缀结尾。虽然这也可以在StringBuilder类的帮助下在每个字符串之后附加分隔符,但StringJoiner提供了简单的方法来实现,而无需编写大量代码。
+
+StringJoiner类共有2个构造函数,5个公有方法。其中最常用的方法就是add方法和toString方法,类似于StringBuilder中的append方法和toString方法。
+
+### 用法
+
+StringJoiner的用法比较简单,下面的代码中,我们使用StringJoiner进行了字符串拼接。
+
+ public class StringJoinerTest {
+
+ public static void main(String[] args) {
+ StringJoiner sj = new StringJoiner("Hollis");
+
+ sj.add("hollischuang");
+ sj.add("Java干货");
+ System.out.println(sj.toString());
+
+ StringJoiner sj1 = new StringJoiner(":","[","]");
+
+ sj1.add("Hollis").add("hollischuang").add("Java干货");
+ System.out.println(sj1.toString());
+ }
+ }
+
+
+以上代码输出结果:
+
+ hollischuangHollisJava干货
+ [Hollis:hollischuang:Java干货]
+
+
+值得注意的是,当我们`StringJoiner(CharSequence delimiter)`初始化一个`StringJoiner`的时候,这个`delimiter`其实是分隔符,并不是可变字符串的初始值。
+
+`StringJoiner(CharSequence delimiter,CharSequence prefix,CharSequence suffix)`的第二个和第三个参数分别是拼接后的字符串的前缀和后缀。
+
+### 原理
+
+介绍了简单的用法之后,我们再来看看这个StringJoiner的原理,看看他到底是如何实现的。主要看一下add方法:
+
+ public StringJoiner add(CharSequence newElement) {
+ prepareBuilder().append(newElement);
+ return this;
+ }
+
+ private StringBuilder prepareBuilder() {
+ if (value != null) {
+ value.append(delimiter);
+ } else {
+ value = new StringBuilder().append(prefix);
+ }
+ return value;
+ }
+
+
+看到了一个熟悉的身影——StringBuilder ,没错,StringJoiner其实就是依赖StringBuilder实现的。
+
+当我们发现StringJoiner其实是通过StringBuilder实现之后,我们大概就可以猜到,**他的性能损耗应该和直接使用StringBuilder差不多**!
+
+### 为什么需要StringJoiner
+
+在了解了StringJoiner的用法和原理后,可能很多读者就会产生一个疑问,明明已经有一个StringBuilder了,为什么Java 8中还要定义一个StringJoiner呢?到底有什么好处呢?
+
+如果读者足够了解Java 8的话,或许可以猜出个大概,这肯定和Stream有关。
+
+作者也在[Java doc][3]中找到了答案:
+
+> A StringJoiner may be employed to create formatted output from a Stream using Collectors.joining(CharSequence)
+
+试想,在Java中,如果我们有这样一个List:
+
+ List list = ImmutableList.of("Hollis","hollischuang","Java干货");
+
+
+如果我们想要把他拼接成一个以下形式的字符串:
+
+ Hollis,hollischuang,Java干货
+
+
+可以通过以下方式:
+
+ StringBuilder builder = new StringBuilder();
+
+ if (!list.isEmpty()) {
+ builder.append(list.get(0));
+ for (int i = 1, n = list.size(); i < n; i++) {
+ builder.append(",").append(list.get(i));
+ }
+ }
+ builder.toString();
+
+
+还可以使用:
+
+ list.stream().reduce(new StringBuilder(), (sb, s) -> sb.append(s).append(','), StringBuilder::append).toString();
+
+
+但是输出结果稍有些不同,需要进行二次处理:
+
+ Hollis,hollischuang,Java干货,
+
+
+还可以使用"+"进行拼接:
+
+ list.stream().reduce((a,b)->a + "," + b).toString();
+
+
+以上几种方式,要么是代码复杂,要么是性能不高,或者无法直接得到想要的结果。
+
+为了满足类似这样的需求,Java 8中提供的StringJoiner就派上用场了。以上需求只需要一行代码:
+
+ list.stream().collect(Collectors.joining(":"))
+
+
+即可。上面用的表达式中,Collectors.joining的源代码如下:
+
+ public static Collector joining(CharSequence delimiter,
+ CharSequence prefix,
+ CharSequence suffix) {
+ return new CollectorImpl<>(
+ () -> new StringJoiner(delimiter, prefix, suffix),
+ StringJoiner::add, StringJoiner::merge,
+ StringJoiner::toString, CH_NOID);
+ }
+
+
+其实现原理就是借助了StringJoiner。
+
+当然,或许在`Collector`中直接使用`StringBuilder`似乎也可以实现类似的功能,只不过稍微麻烦一些。所以,Java 8中提供了`StringJoiner`来丰富`Stream`的用法。
+
+而且`StringJoiner`也可以方便的增加前缀和后缀,比如我们希望得到的字符串是`[Hollis,hollischuang,Java干货]`而不是`Hollis,hollischuang,Java`干货的话,StringJoiner的优势就更加明显了。
+
+### 总结
+
+本文介绍了Java 8中提供的可变字符串类——StringJoiner,可以用于字符串拼接。
+
+StringJoiner其实是通过StringBuilder实现的,所以他的性能和StringBuilder差不多,他也是非线程安全的。
+
+如果日常开发中中,需要进行字符串拼接,如何选择?
+
+1、如果只是简单的字符串拼接,考虑直接使用"+"即可。
+
+2、如果是在for循环中进行字符串拼接,考虑使用`StringBuilder`和`StringBuffer`。
+
+3、如果是通过一个`List`进行字符串拼接,则考虑使用`StringJoiner`。
+
+ [1]: http://www.hollischuang.com/archives/3186
+ [2]: http://www.hollischuang.com/wp-content/uploads/2019/02/15508994967943.jpg
+ [3]: https://docs.oracle.com/javase/8/docs/api/java/util/StringJoiner.html
\ No newline at end of file
diff --git a/docs/basics/java-basic/substring.md b/docs/basics/java-basic/substring.md
index a017b164..101f97d2 100644
--- a/docs/basics/java-basic/substring.md
+++ b/docs/basics/java-basic/substring.md
@@ -1,4 +1,6 @@
-String是Java中一个比较基础的类,每一个开发人员都会经常接触到。而且,String也是面试中经常会考的知识点。String有很多方法,有些方法比较常用,有些方法不太常用。今天要介绍的substring就是一个比较常用的方法,而且围绕substring也有很多面试题。
+String是Java中一个比较基础的类,每一个开发人员都会经常接触到。而且,String也是面试中经常会考的知识点。
+
+String有很多方法,有些方法比较常用,有些方法不太常用。今天要介绍的substring就是一个比较常用的方法,而且围绕substring也有很多面试题。
`substring(int beginIndex, int endIndex)`方法在不同版本的JDK中的实现是不同的。了解他们的区别可以帮助你更好的使用他。为简单起见,后文中用`substring()`代表`substring(int beginIndex, int endIndex)`方法。
diff --git a/docs/basics/java-basic/switch-string.md b/docs/basics/java-basic/switch-string.md
index 1498aab7..067e7ad4 100644
--- a/docs/basics/java-basic/switch-string.md
+++ b/docs/basics/java-basic/switch-string.md
@@ -1,7 +1,5 @@
Java 7中,switch的参数可以是String类型了,这对我们来说是一个很方便的改进。到目前为止switch支持这样几种数据类型:`byte` `short` `int` `char` `String` 。但是,作为一个程序员我们不仅要知道他有多么好用,还要知道它是如何实现的,switch对整型的支持是怎么实现的呢?对字符型是怎么实现的呢?String类型呢?有一点Java开发经验的人这个时候都会猜测switch对String的支持是使用equals()方法和hashcode()方法。那么到底是不是这两个方法呢?接下来我们就看一下,switch到底是如何实现的。
-
-
### 一、switch对整型支持的实现
下面是一段很简单的Java代码,定义一个int型变量a,然后使用switch语句进行判断。执行这段代码输出内容为5,那么我们将下面这段代码反编译,看看他到底是怎么实现的。
@@ -71,7 +69,7 @@ Java 7中,switch的参数可以是String类型了,这对我们来说是一
}
-编译后的代码如下: `public class switchDemoChar
+编译后的代码如下:
public class switchDemoChar
{
@@ -147,4 +145,4 @@ Java 7中,switch的参数可以是String类型了,这对我们来说是一
看到这个代码,你知道原来字符串的switch是通过`equals()`和`hashCode()`方法来实现的。**记住,switch中只能使用整型**,比如`byte`。`short`,`char`(ackii码是整型)以及`int`。还好`hashCode()`方法返回的是`int`,而不是`long`。通过这个很容易记住`hashCode`返回的是`int`这个事实。仔细看下可以发现,进行`switch`的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。因为Java编译器只增加了一个`equals`方法,如果你比较的是字符串字面量的话会非常快,比如”abc” ==”abc”。如果你把`hashCode()`方法的调用也考虑进来了,那么还会再多一次的调用开销,因为字符串一旦创建了,它就会把哈希值缓存起来。因此如果这个`switch`语句是用在一个循环里的,比如逐项处理某个值,或者游戏引擎循环地渲染屏幕,这里`hashCode()`方法的调用开销其实不会很大。
-好,以上就是关于switch对整型、字符型、和字符串型的支持的实现方式,总结一下我们可以发现,**其实switch只支持一种数据类型,那就是整型,其他数据类型都是转换成整型之后在使用switch的。**
+好,以上就是关于switch对整型、字符型、和字符串型的支持的实现方式,总结一下我们可以发现,**其实switch只支持一种数据类型,那就是整型,其他数据类型都是转换成整型之后再使用switch的。**
diff --git a/docs/basics/java-basic/synchronized-vs-asynchronization.md b/docs/basics/java-basic/synchronized-vs-asynchronization.md
index d3cded25..79fd30cf 100644
--- a/docs/basics/java-basic/synchronized-vs-asynchronization.md
+++ b/docs/basics/java-basic/synchronized-vs-asynchronization.md
@@ -20,5 +20,5 @@
3 老张把响水壶放到火上,一直在水壶旁等着水开。(异步阻塞)
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
-1和2的区别是,调用方在得到返回之前所做的事情不一行。
-1和3的区别是,被调用方对于烧水的处理不一样。
\ No newline at end of file
+1和2的区别是,调用方在得到返回之前所做的事情不一样。
+1和3的区别是,被调用方对于烧水的处理不一样。
diff --git a/docs/basics/java-basic/synchronizedlist-vector.md b/docs/basics/java-basic/synchronizedlist-vector.md
index 4da3300f..a673eb7d 100644
--- a/docs/basics/java-basic/synchronizedlist-vector.md
+++ b/docs/basics/java-basic/synchronizedlist-vector.md
@@ -128,9 +128,15 @@ ArrayList类的remove方法内容如下:
> 从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。
-**同步代码块和同步方法的区别** 1.同步代码块在锁定的范围上可能比同步方法要小,一般来说锁的范围大小和性能是成反比的。 2.同步块可以更加精确的控制锁的作用域(锁的作用域就是从锁被获取到其被释放的时间),同步方法的锁的作用域就是整个方法。 3.静态代码块可以选择对哪个对象加锁,但是静态方法只能给this对象加锁。
+**同步代码块和同步方法的区别**
-> 因为SynchronizedList只是使用同步代码块包裹了ArrayList的方法,而ArrayList和Vector中同名方法的方法体内容并无太大差异,所以在锁定范围和锁的作用域上两者并无却别。 在锁定的对象区别上,SynchronizedList的同步代码块锁定的是mutex对象,Vector锁定的是this对象。那么mutex对象又是什么呢? 其实SynchronizedList有一个构造函数可以传入一个Object,如果在调用的时候显示的传入一个对象,那么锁定的就是用户传入的对象。如果没有指定,那么锁定的也是this对象。
+1.同步代码块在锁定的范围上可能比同步方法要小,一般来说锁的范围大小和性能是成反比的。
+
+2.同步块可以更加精确的控制锁的作用域(锁的作用域就是从锁被获取到其被释放的时间),同步方法的锁的作用域就是整个方法。
+
+3.同步代码块可以选择对哪个对象加锁,但是静态方法只能给this对象加锁。
+
+> 因为SynchronizedList只是使用同步代码块包裹了ArrayList的方法,而ArrayList和Vector中同名方法的方法体内容并无太大差异,所以在锁定范围和锁的作用域上两者并无区别。 在锁定的对象区别上,SynchronizedList的同步代码块锁定的是mutex对象,Vector锁定的是this对象。那么mutex对象又是什么呢? 其实SynchronizedList有一个构造函数可以传入一个Object,如果在调用的时候显示的传入一个对象,那么锁定的就是用户传入的对象。如果没有指定,那么锁定的也是this对象。
所以,SynchronizedList和Vector的区别目前为止有两点: 1.如果使用add方法,那么他们的扩容机制不一样。 2.SynchronizedList可以指定锁定的对象。
@@ -140,4 +146,4 @@ ArrayList类的remove方法内容如下:
之前的比较都是基于我们将ArrayList转成SynchronizedList。那么如果我们想把LinkedList变成线程安全的,或者说我想要方便在中间插入和删除的同步的链表,那么我可以将已有的LinkedList直接转成 SynchronizedList,而不用改变他的底层数据结构。而这一点是Vector无法做到的,因为他的底层结构就是使用数组实现的,这个是无法更改的。
-所以,最后,SynchronizedList和Vector最主要的区别: **1\.SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。** **2\.使用SynchronizedList的时候,进行遍历时要手动进行同步处理**。 **3\.SynchronizedList可以指定锁定的对象。**
\ No newline at end of file
+所以,最后,SynchronizedList和Vector最主要的区别: **1\.SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。** **2\.使用SynchronizedList的时候,进行遍历时要手动进行同步处理**。 **3\.SynchronizedList可以指定锁定的对象。**
diff --git a/docs/basics/java-basic/syntactic-sugar.md b/docs/basics/java-basic/syntactic-sugar.md
index f1dc7f5b..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`类,所以枚举类型不能被继承。**
### 糖块六 、 内部类
@@ -682,7 +682,7 @@ Java里,对于文件操作IO流、数据库连接等开销非常昂贵的资
}
-上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List另一个是List ,但是,这段代码是编译通不过的。因为我们前面讲过,参数List和List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。
+上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是`List`另一个是`List` ,但是,这段代码是编译通不过的。因为我们前面讲过,参数`List`和`List`编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。
**二、当泛型遇到catch** 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型`MyException`和`MyException`的
diff --git a/docs/basics/java-basic/type-erasue.md b/docs/basics/java-basic/type-erasure.md
similarity index 82%
rename from docs/basics/java-basic/type-erasue.md
rename to docs/basics/java-basic/type-erasure.md
index f1f40ea4..8e4e1dfd 100644
--- a/docs/basics/java-basic/type-erasue.md
+++ b/docs/basics/java-basic/type-erasure.md
@@ -4,21 +4,21 @@
通常情况下,一个编译器处理泛型有两种方式:
-1\.`Code specialization`。在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。例如,针对一个泛型`list`,可能需要 针对`string`,`integer`,`float`产生三份目标代码。
+1\.`Code specialization`。在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。例如,针对一个泛型`List`,可能需要 针对`String`,`Integer`,`Float`产生三份目标代码。
2\.`Code sharing`。对每个泛型类只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。
-**C++**中的模板(`template`)是典型的`Code specialization`实现。**C++**编译器会为每一个泛型类实例生成一份执行代码。执行代码中`integer list`和`string list`是两种不同的类型。这样会导致**代码膨胀(code bloat)**。 **C#**里面泛型无论在程序源码中、编译后的`IL`中(Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,`List`与`List`就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为`真实泛型`。 **Java**语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,`ArrayList`与`ArrayList`就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为**类型擦除**,基于这种方法实现的泛型被称为`伪泛型`。
+**C++** 中的模板(`template`)是典型的`Code specialization`实现。**C++** 编译器会为每一个泛型类实例生成一份执行代码。执行代码中`Integer List`和`String List`是两种不同的类型。这样会导致**代码膨胀(code bloat)**。 **C#** 里面泛型无论在程序源码中、编译后的`IL`中(Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,`List`与`List`就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为`真实泛型`。 **Java**语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,`ArrayList`与`ArrayList`就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为**类型擦除**,基于这种方法实现的泛型被称为`伪泛型`。
`C++`和`C#`是使用`Code specialization`的处理机制,前面提到,他有一个缺点,那就是**会导致代码膨胀**。另外一个弊端是在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用`Code sharing`方式处理泛型的主要原因。
-`Java`编译器通过`Code sharing`方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过**类型擦除**(`type erasue`)实现的。
+`Java`编译器通过`Code sharing`方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过**类型擦除**(`type erasure`)实现的。
* * *
### 二、什么是类型擦除
-前面我们多次提到这个词:**类型擦除**(`type erasue`)**,那么到底什么是类型擦除呢?
+前面我们多次提到这个词:**类型擦除**(`type erasure`),那么到底什么是类型擦除呢?
> 类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。 类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。 类型擦除的主要过程如下: 1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。(这部分内容可以看:[Java泛型中extends和super的理解][2]) 2.移除所有的类型参数。
@@ -35,7 +35,7 @@
System.out.println(map.get("name"));
System.out.println(map.get("age"));
}
-
+
**反编译后的code 1:**
@@ -46,7 +46,7 @@
System.out.println((String) map.get("name"));
System.out.println((String) map.get("age"));
}
-
+
我们发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型,
@@ -73,7 +73,7 @@
return this.value - that.value;
}
}
-
+
**反编译后的code 2:**
@@ -102,7 +102,6 @@
}
private byte value;
}
-
* * *
@@ -120,7 +119,7 @@
return w;
}
}
-
+
**反编译后的code 3:**
@@ -142,7 +141,7 @@
return w;
}
}
-
+
第2个泛型类`Comparable `擦除后 A被替换为最左边界`Object`。`Comparable`的类型参数`NumericValue`被擦除掉,但是这直 接导致`NumericValue`没有实现接口`Comparable的compareTo(Object that)`方法,于是编译器充当好人,添加了一个**桥接方法**。 第3个示例中限定了类型参数的边界`>A`,A必须为`Comparable`的子类,按照类型擦除的过程,先讲所有的类型参数 ti换为最左边界`Comparable`,然后去掉参数类型`A`,得到最终的擦除后结果。
@@ -162,13 +161,13 @@
System.out.println("invoke method(List list)");
}
}
-
+
上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是`List`另一个是`List` ,但是,这段代码是编译通不过的。因为我们前面讲过,参数`List`和`List`编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。
**二、当泛型遇到catch:**
-如果我们自定义了一个泛型异常类GenericException,那么,不要尝试用多个catch取匹配不同的异常类型,例如你想要分别捕获GenericException、GenericException,这也是有问题的。
+如果我们自定义了一个泛型异常类GenericException,那么,不要尝试用多个catch取匹配不同的异常类型,例如你想要分别捕获GenericException、GenericException,这也是有问题的。
三、当泛型内包含静态变量
@@ -185,7 +184,7 @@
public static int var=0;
public void nothing(T x){}
}
-
+
答案是——2!由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。
@@ -195,5 +194,5 @@
1\.虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在`List`.class或是`List.class`,而只有`List.class`。 2.创建泛型对象时请指明类型,让编译器尽早的做参数检查(**Effective Java,第23条:请不要在新代码中使用原生态类型**) 3.不要忽略编译器的警告信息,那意味着潜在的`ClassCastException`等着你。 4.静态变量是被泛型类的所有实例所共享的。对于声明为`MyClass`的类,访问其中的静态变量的方法仍然是 `MyClass.myStaticVar`。不管是通过`new MyClass`还是`new MyClass`创建的对象,都是共享一个静态变量。 5.泛型的类型参数不能用在`Java`异常处理的`catch`语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,`JVM`是无法区分两个异常类型`MyException`和`MyException`的。对于`JVM`来说,它们都是 `MyException`类型的。也就无法执行与异常对应的`catch`语句。
- [1]: http://docs.oracle.com/javase/tutorial/java/generics/erasure.html
- [2]: /archives/255
\ No newline at end of file
+[1]: http://docs.oracle.com/javase/tutorial/java/generics/erasure.html
+[2]: /archives/255
diff --git a/docs/basics/java-basic/value-of-vs-to-string.md b/docs/basics/java-basic/value-of-vs-to-string.md
index a3ed3fe4..9a59cfa9 100644
--- a/docs/basics/java-basic/value-of-vs-to-string.md
+++ b/docs/basics/java-basic/value-of-vs-to-string.md
@@ -1,4 +1,4 @@
-我们有三种方式将一个int类型的变量变成呢过String类型,那么他们有什么区别?
+我们有三种方式将一个int类型的变量变成一个String类型,那么他们有什么区别?
1.int i = 5;
2.String i1 = "" + i;
@@ -7,4 +7,4 @@
第三行和第四行没有任何区别,因为String.valueOf(i)也是调用Integer.toString(i)来实现的。
-第二行代码其实是String i1 = (new StringBuilder()).append(i).toString();,首先创建一个StringBuilder对象,然后再调用append方法,再调用toString方法。
\ No newline at end of file
+第二行代码其实是String i1 = (new StringBuilder()).append(i).toString();,首先创建一个StringBuilder对象,然后再调用append方法,再调用toString方法。
diff --git a/docs/basics/java-basic/variable.md b/docs/basics/java-basic/variable.md
deleted file mode 100644
index 8e715c7c..00000000
--- a/docs/basics/java-basic/variable.md
+++ /dev/null
@@ -1,29 +0,0 @@
-### 类变量、成员变量和局部变量
-
-Java中共有三种变量,分别是类变量、成员变量和局部变量。他们分别存放在JVM的方法区、堆内存和栈内存中。
-```java
- /**
- * @author Hollis
- */
- public class Variables {
-
- /**
- * 类变量
- */
- private static int a;
-
- /**
- * 成员变量
- */
- private int b;
-
- /**
- * 局部变量
- * @param c
- */
- public void test(int c){
- int d;
- }
- }
-```
-上面定义的三个变量中,变量a就是类变量,变量b就是成员变量,而变量c和d是局部变量。
diff --git a/docs/basics/object-oriented/characteristics.md b/docs/basics/object-oriented/characteristics.md
index 5b3b3bc0..0ac4f280 100644
--- a/docs/basics/object-oriented/characteristics.md
+++ b/docs/basics/object-oriented/characteristics.md
@@ -1,15 +1,90 @@
-
+我们说面向对象的开发范式,其实是对现实世界的理解和抽象的方法,那么,具体如何将现实世界抽象成代码呢?这就需要运用到面向对象的三大特性,分别是封装性、继承性和多态性。
### 封装(Encapsulation)
-所谓封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
+所谓封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
+
+简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
+
+#### 封装举例
+
+如我们想要定义一个矩形,先定义一个Rectangle类,并其中通过封装的手段放入一些必备数据。
+ /**
+ * 矩形
+ */
+ class Rectangle {
+
+ /**
+ * 设置矩形的长度和宽度
+ */
+ public Rectangle(int length, int width) {
+ this.length = length;
+ this.width = width;
+ }
+
+ /**
+ * 长度
+ */
+ private int length;
+
+ /**
+ * 宽度
+ */
+ private int width;
+
+ /**
+ * 获得矩形面积
+ *
+ * @return
+ */
+ public int area() {
+ return this.length * this.width;
+ }
+ }
+
+我们通过封装的方式,给"矩形"定义了"长度"和"宽度",这就完成了对现实世界中的"矩形"的抽象的第一步。
+
### 继承(Inheritance)
-继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;
+继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
+
+通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。
+
+#### 继承举例
+
+我们想要定义一个正方形,因为已经有了矩形,所以我们可以直接继承Rectangle类,因为正方形是长方形的一种特例。
+
+
+ /**
+ * 正方形,继承自矩形
+ */
+ class Square extends Rectangle {
+
+ /**
+ * 设置正方形边长
+ *
+ * @param length
+ */
+ public Square(int length) {
+ super(length, length);
+ }
+ }
+
+现实世界中,"正方形"是"矩形"的特例,或者说正方形是通过矩形派生出来的,这种派生关系,在面向对象中可以用继承来表达。
### 多态(Polymorphism)
-所谓多态就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
+所谓多态就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。
+
+这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
+
+最常见的多态就是将子类传入父类参数中,运行时调用父类方法时通过传入的子类决定具体的内部结构或行为。
+
+关于多态的例子,我们第二章中深入开展介绍。
+
+在介绍了面向对象的封装、继承、多态的三个基本特征之后,我们基本掌握了对现实世界抽象的基本方法。
+
+莎士比亚说:"一千个读者眼里有一千个哈姆雷特",说到对现实世界的抽象,虽然方法相同,但是运用同样的方法,最终得到的结果可能千差万别,那么如何评价这个抽象的结果的好坏呢?
-最常见的多态就是将子类传入父类参数中,运行时调用父类方法时通过传入的子类决定具体的内部结构或行为。
\ No newline at end of file
+这就要提到面喜爱那个对象的五大基本原则了,有了五大原则,我们参考他们来评价一个抽象的好坏。
\ No newline at end of file
diff --git a/docs/basics/object-oriented/constructor.md b/docs/basics/object-oriented/constructor.md
index a0b3c011..894a6c26 100644
--- a/docs/basics/object-oriented/constructor.md
+++ b/docs/basics/object-oriented/constructor.md
@@ -1,5 +1,56 @@
-构造函数,是一种特殊的方法。 主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。 特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。
+构造函数,是一种特殊的方法。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。
-构造函数跟一般的实例方法十分相似;但是与其它方法不同,构造器没有返回类型,不会被继承,且可以有范围修饰符。构造器的函数名称必须和它所属的类的名称相同。 它承担着初始化对象数据成员的任务。
+ /**
+ * 矩形
+ */
+ class Rectangle {
+
+ /**
+ * 构造函数
+ */
+ public Rectangle(int length, int width) {
+ this.length = length;
+ this.width = width;
+ }
+
+ public static void main (String []args){
+ //使用构造函数创建对象
+ Rectangle rectangle = new Rectangle(10,5);
+
+ }
+ }
+
+特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。
+
+
+构造函数跟一般的实例方法十分相似;但是与其它方法不同,构造器没有返回类型,不会被继承,且可以有范围修饰符。
+
+构造器的函数名称必须和它所属的类的名称相同。它承担着初始化对象数据成员的任务。
如果在编写一个可实例化的类时没有专门编写构造函数,多数编程语言会自动生成缺省构造器(默认构造函数)。默认构造函数一般会把成员变量的值初始化为默认值,如int -> 0,Integer -> null。
+
+
+如果在编写一个可实例化的类时没有专门编写构造函数,默认情况下,一个Java类中会自动生成一个默认无参构造函数。默认构造函数一般会把成员变量的值初始化为默认值,如int -> 0,Integer -> null。
+
+但是,如果我们手动在某个类中定义了一个有参数的构造函数,那么这个默认的无参构造函数就不会自动添加了。需要手动创建!
+
+ /**
+ * 矩形
+ */
+ class Rectangle {
+
+ /**
+ * 构造函数
+ */
+ public Rectangle(int length, int width) {
+ this.length = length;
+ this.width = width;
+ }
+
+ /**
+ * 无参构造函数
+ */
+ public Rectangle() {
+
+ }
+ }
diff --git a/docs/basics/object-oriented/extends-implement.md b/docs/basics/object-oriented/extends-implement.md
index 0d395526..ca79513b 100644
--- a/docs/basics/object-oriented/extends-implement.md
+++ b/docs/basics/object-oriented/extends-implement.md
@@ -1,27 +1,30 @@
-面向对象有三个特征:封装、继承、多态。
+前面的章节我们提到过面向对象有三个特征:封装、继承、多态。前面我们分别介绍过了这三个特性。
-其中继承和实现都体现了`传递性`。而且明确定义如下:
+我们知道,继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。这种派生方式体现了*传递性*。
->继承:如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们的相同部分都放到父类里,让他们都继承这个类。
->
->实现:如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标
+在Java中,除了继承,还有一种体现传递性的方式叫做实现。那么,这两者方式有什么区别呢?
+继承和实现两者的明确定义和区别如下:
-所以,继承的根本原因是因为要*复用*,而实现的根本原因是需要定义一个*标准*。
+继承(Inheritance):如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们的相同部分都放到父类里,让他们都继承这个类。
-在Java中,继承使用`extends`关键字实现,而实现通过`implements`关键字。
+实现(Implement):如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标
-Java中支持一个类同时实现多个接口,但是不支持同时继承多个类。
+继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。所以,继承的根本原因是因为要*复用*,而实现的根本原因是需要定义一个*标准*。
+
+在Java中,继承使用`extends`关键字实现,而实现通过`implements`关键字。
->简单点说,就是同样是一台汽车,既可以是电动车,也可以是汽油车,也可以是油电混合的,只要实现不同的标准就行了,但是一台车只能属于一个品牌,一个厂商。
+ >简单点说,就是同样是一台汽车,既可以是电动车,也可以是汽油车,也可以是油电混合的,只要实现不同的标准就行了,但是一台车只能属于一个品牌,一个厂商。
+
+ ```
+ class Car extends Benz implements GasolineCar, ElectroCar{
+
+ }
```
-class Car extends Benz implements GasolineCar, ElectroCar{
-
-}
+以上,我们定义了一辆汽车,他实现了电动车和汽油车两个标准,但是他属于奔驰这个品牌。像上面这样定义,我们可以最大程度的遵守标准,并且复用奔驰车所有已有的一些功能组件。
-```
-
-在接口中只能定义全局常量(static final)和无实现的方法(Java 8以后可以有defult方法);而在继承中可以定义属性方法,变量,常量等。
+另外,在接口中只能定义全局常量(static final)和无实现的方法(Java 8以后可以有default方法);而在继承中可以定义属性方法,变量,常量等。
+*特别需要注意的是,Java中支持一个类同时实现多个接口,但是不支持同时继承多个类。* 但是这个问题在Java 8之后也不绝对了。关于多继承的问题,我们下一个章节中介绍。
\ No newline at end of file
diff --git a/docs/basics/object-oriented/inheritance-composition.md b/docs/basics/object-oriented/inheritance-composition.md
index 5eff0d32..14b70340 100644
--- a/docs/basics/object-oriented/inheritance-composition.md
+++ b/docs/basics/object-oriented/inheritance-composition.md
@@ -1,6 +1,10 @@
-Java是一个面向对象的语言。每一个学习过Java的人都知道,封装、继承、多态是面向对象的三个特征。每个人在刚刚学习继承的时候都会或多或少的有这样一个印象:继承可以帮助我实现类的复用。所以,很多开发人员在需要复用一些代码的时候会很自然的使用类的继承的方式,因为书上就是这么写的(老师就是这么教的)。但是,其实这样做是不对的。长期大量的使用继承会给代码带来很高的维护成本。
+在前面几篇文章中,我们了解了封装、继承、多态是面向对象的三个特征。并且通过对继承和实现的学习,了解到继承可以帮助我实现类的复用。
-本文将介绍组合和继承的概念及区别,并从多方面分析在写代码时如何进行选择。
+所以,很多开发人员在需要复用一些代码的时候会很自然的使用类的继承的方式。
+
+但是,遇到想要复用的场景就直接使用继承,这样做是不对的。长期大量的使用继承会给代码带来很高的维护成本。
+
+本文将介绍一种可以帮助我们复用的新的概念——组合,通过学习组合和继承的概念及区别,并从多方面帮大家分析在写代码时如何进行选择。
## 面向对象的复用技术
@@ -12,7 +16,9 @@ Java是一个面向对象的语言。每一个学习过Java的人都知道,封
## 继承
-继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;继承是一种[`is-a`][1]关系。(图片来自网络,侵删。)
+前面的章节中重点介绍过继承,我们说继承是类与类或者接口与接口之间最常见的一种关系;继承是一种[`is-a`][1]关系。
+
+> is-a:表示"是一个"的关系,如狗是一个动物
![Inheritance][2]
@@ -20,6 +26,8 @@ Java是一个面向对象的语言。每一个学习过Java的人都知道,封
组合(Composition)体现的是整体与部分、拥有的关系,即[`has-a`][3]的关系。
+> has-a:表示"有一个"的关系,如狗有一个尾巴
+
![Composition][4]
## 组合与继承的区别和联系
diff --git a/docs/basics/object-oriented/java-pass-by.md b/docs/basics/object-oriented/java-pass-by.md
index 895f2a2f..7d2abf93 100644
--- a/docs/basics/object-oriented/java-pass-by.md
+++ b/docs/basics/object-oriented/java-pass-by.md
@@ -1,57 +1,98 @@
+关于Java中方法间的参数传递到底是怎样的、为什么很多人说Java只有值传递等问题,一直困惑着很多人,甚至我在面试的时候问过很多有丰富经验的开发者,他们也很难解释的很清楚。
+
+我很久也写过一篇文章,我当时认为我把这件事说清楚了,但是,最近在整理这部分知识点的时候,我发现我当时理解的还不够透彻,于是我想着通过Google看看其他人怎么理解的,但是遗憾的是没有找到很好的资料可以说的很清楚。
+
+于是,我决定尝试着把这个话题总结一下,重新理解一下这个问题。
+
+### 辟谣时间
+
+关于这个问题,在StackOverflow上也引发过广泛的讨论,看来很多程序员对于这个问题的理解都不尽相同,甚至很多人理解的是错误的。还有的人可能知道Java中的参数传递是值传递,但是说不出来为什么。
+
+在开始深入讲解之前,有必要纠正一下大家以前的那些错误看法了。如果你有以下想法,那么你有必要好好阅读本文。
+
+> 错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。
+>
+> 错误理解二:Java是引用传递。
+>
+> 错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。
### 实参与形参
我们都知道,在Java中定义方法的时候是可以定义参数的。比如Java中的main方法,`public static void main(String[] args)`,这里面的args就是参数。参数在程序语言中分为形式参数和实际参数。
> 形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。
->
+>
> 实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。
简单举个例子:
-```
-public static void main(String[] args) {
- ParamTest pt = new ParamTest();
- pt.sout("Hollis");//实际参数为 Hollis
-}
-
-public void sout(String name) { //形式参数为 name
- System.out.println(name);
-}
-```
+ public static void main(String[] args) {
+ ParamTest pt = new ParamTest();
+ pt.sout("Hollis");//实际参数为 Hollis
+ }
+
+ public void sout(String name) { //形式参数为 name
+ System.out.println(name);
+ }
+
实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。
-### 值传递与引用传递
+### 求值策略
+
+我们说当进行方法调用的时候,需要把实际参数传递给形式参数,那么传递的过程中到底传递的是什么东西呢?
+
+这其实是程序设计中**求值策略(Evaluation strategies)**的概念。
+
+在计算机科学中,求值策略是确定编程语言中表达式的求值的一组(通常确定性的)规则。求值策略定义何时和以何种顺序求值给函数的实际参数、什么时候把它们代换入函数、和代换以何种形式发生。
+
+求值策略分为两大基本类,基于如何处理给函数的实际参数,分位严格的和非严格的。
+
+#### 严格求值
+
+在“严格求值”中,函数调用过程中,给函数的实际参数总是在应用这个函数之前求值。多数现存编程语言对函数都使用严格求值。所以,我们本文只关注严格求值。
+
+在严格求值中有几个关键的求值策略是我们比较关心的,那就是**传值调用**(Call by value)、**传引用调用**(Call by reference)以及**传共享对象调用**(Call by sharing)。
+
+* 传值调用(值传递)
+ * 在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。因为形式参数拿到的只是一个"局部拷贝",所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。
+* 传引用调用(引用传递)
+ * 在传引用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。因为传递的是引用,所以,如果在被调函数中改变了形式参数的值,改变对于调用者来说是可见的。
+* 传共享对象调用(共享对象传递)
+ * 传共享对象调用中,先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。因为参数的地址都指向同一个对象,所以我们称也之为"传共享对象",所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。
+
+不知道大家有没有发现,其实传共享对象调用和传值调用的过程几乎是一样的,都是进行"求值"、"拷贝"、"传递"。你品,你细品。
+
+![][1]
+
+但是,传共享对象调用和内传引用调用的结果又是一样的,都是在被调函数中如果改变参数的内容,那么这种改变也会对调用者有影响。你再品,你再细品。
+
+那么,共享对象传递和值传递以及引用传递之间到底有很么关系呢?
-上面提到了,当我们调用一个有参函数的时候,会把实际参数传递给形式参数。但是,在程序语言中,这个传递过程中传递的两种情况,即值传递和引用传递。我们来看下程序语言中是如何定义和区分值传递和引用传递的。
+对于这个问题,我们应该关注过程,而不是结果,**因为传共享对象调用的过程和传值调用的过程是一样的,而且都有一步关键的操作,那就是"复制",所以,通常我们认为传共享对象调用是传值调用的特例**
-> 值传递(pass by value)是指在调用函数时将实际参数`复制`一份传递到函数中,这样在函数中如果对`参数`进行修改,将不会影响到实际参数。
->
-> 引用传递(pass by reference)是指在调用函数时将实际参数的地址`直接`传递到函数中,那么在函数中对`参数`所进行的修改,将影响到实际参数。
+我们先把传共享对象调用放在一边,我们再来回顾下传值调用和传引用调用的主要区别:
-那么,我来给大家总结一下,值传递和引用传递之前的区别的重点是什么:
+**传值调用是指在调用函数时将实际参数`复制`一份传递到函数中,传引用调用是指在调用函数时将实际参数的引用`直接`传递到函数中。**
-
+![pass-by-reference-vs-pass-by-value-animation][2]
-这里我们来举一个形象的例子。再来深入理解一下值传递和引用传递:
+所以,两者的最主要区别就是是直接传递的,还是传递的是一个副本。
-你有一把钥匙,当你的朋友想要去你家的时候,如果你`直接`把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。
+这里我们来举一个形象的例子。再来深入理解一下传值调用和传引用调用:
-你有一把钥匙,当你的朋友想要去你家的时候,你`复刻`了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。
+你有一把钥匙,当你的朋友想要去你家的时候,如果你`直接`把你的钥匙给他了,这就是引用传递。
+这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。
-### 参考资料
+你有一把钥匙,当你的朋友想要去你家的时候,你`复刻`了一把新钥匙给他,自己的还在自己手里,这就是值传递。
-[Evaluation strategy][7]
+这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。
-[关于值传递和引用传递][8]
+前面我们介绍过了传值调用、传引用调用以及传值调用的特例传共享对象调用,那么,Java中是采用的哪种求值策略呢?
-[按值传递、按引用传递、按共享传递][9]
+下一篇我们深入分析。
-[Is Java “pass-by-reference” or “pass-by-value”?][2]
-[2]: https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value
-[7]: https://en.wikipedia.org/wiki/Evaluation_strategy
-[8]: http://chenwenbo.github.io/2016/05/11/%E5%85%B3%E4%BA%8E%E5%80%BC%E4%BC%A0%E9%80%92%E5%92%8C%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92/
-[9]: http://menzhongxin.com/2017/02/07/%E6%8C%89%E5%80%BC%E4%BC%A0%E9%80%92-%E6%8C%89%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92%E5%92%8C%E6%8C%89%E5%85%B1%E4%BA%AB%E4%BC%A0%E9%80%92/
+ [1]: http://www.hollischuang.com/wp-content/uploads/2020/04/15865905252659.jpg
+ [2]: http://www.hollischuang.com/wp-content/uploads/2020/04/pass-by-reference-vs-pass-by-value-animation.gif
\ No newline at end of file
diff --git a/docs/basics/object-oriented/multiple-inheritance.md b/docs/basics/object-oriented/multiple-inheritance.md
new file mode 100644
index 00000000..308636b5
--- /dev/null
+++ b/docs/basics/object-oriented/multiple-inheritance.md
@@ -0,0 +1,47 @@
+前面我们提到过:"Java中支持一个类同时实现多个接口,但是不支持同时继承多个类。但是这个问题在Java 8之后也不绝对了。"
+
+那么,是不是又很很想知道,为什么Java中不支持同时继承多个类呢?
+### 多继承
+
+一个类,只有一个父类的情况,我们叫做单继承。而一个类,同时有多个父类的情况,叫做多继承。
+
+在Java中,一个类,只能通过extends关键字继承一个类,不允许多继承。但是,多继承在其他的面向对象语言中是有可能支持的。
+
+像C++就是支持多继承的,主要是因为编程的过程是对现实世界的一种抽象,而现实世界中,确实存在着需要多继承的情况。比如维基百科中关于多继承举了一个例子:
+
+例如,可以创造一个“哺乳类动物”类别,拥有进食、繁殖等的功能;然后定义一个子类型“猫”,它可以从父类继承上述功能。
+
+但是,"猫"还可以作为"宠物"的子类,拥有一些宠物独有的能力。
+
+所以,有些面向对象语言是支持多重继承的。
+
+但是,多年以来,多重继承一直都是一个敏感的话题,反对者指它增加了程序的复杂性与含糊性。
+
+### 菱形继承问题
+
+
+假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。
+
+![][1]
+
+这时候,因为D同时继承了B和C,并且B和C又同时继承了A,那么,D中就会因为多重继承,继承到两份来自A中的属性和方法。
+
+这时候,在使用D的时候,如果想要调用一个定义在A中的方法时,就会出现歧义。
+
+因为这样的继承关系的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。
+
+而C++为了解决菱形继承问题,又引入了**虚继承**。
+
+因为支持多继承,引入了菱形继承问题,又因为要解决菱形继承问题,引入了虚继承。而经过分析,人们发现我们其实真正想要使用多继承的情况并不多。
+
+所以,在 Java 中,不允许“实现多继承”,即一个类不允许继承多个父类。但是 Java 允许“声明多继承”,即一个类可以实现多个接口,一个接口也可以继承多个父接口。由于接口只允许有方法声明而不允许有方法实现(Java 8以前),这就避免了 C++ 中多继承的歧义问题。
+
+
+但是,Java不支持多继承,在Java 8中支持了默认函数(default method )之后就不那么绝对了。
+
+虽然我们还是没办法使用extends同时继承多个类,但是因为有了默认函数,我们有可能通过implements从多个接口中继承到多个默认函数,那么,又如何解决这种情况带来的菱形继承问题呢?
+
+这个问题,我们在后面第20.4章节中单独介绍。
+
+
+ [1]: https://www.hollischuang.com/wp-content/uploads/2021/02/16145019571199.jpg
\ No newline at end of file
diff --git a/docs/basics/object-oriented/object-oriented-vs-procedure-oriented.md b/docs/basics/object-oriented/object-oriented-vs-procedure-oriented.md
index d10394c3..cb988ce5 100644
--- a/docs/basics/object-oriented/object-oriented-vs-procedure-oriented.md
+++ b/docs/basics/object-oriented/object-oriented-vs-procedure-oriented.md
@@ -1,39 +1,41 @@
+相信很多Java开发者,在最初接触Java的时候就听说过,Java是一种面向对象的开发语言,那么什么是面向对象呢?
+首先,所谓面向对象,其实是指软件工程中的一类编程风格,很多人称呼他们为开发范式、编程泛型(Programming Paradigm)。面向对象是众多开发范式中的一种。除了面向对象以外,还有面向过程、指令式编程、函数式编程等。
-### 什么是面向过程?
+虽然这几年函数式编程越来越被人们所熟知,但是,在所有的开发范式中,我们接触最多的主要还是面向过程和面向对象两种。
-#### 概述: 自顶而下的编程模式.
+那么,在本书的第一章的第一篇,我们来简单介绍下,什么是面向过程和面向对象。
-把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。
-就是说,在进行面向过程编程的时候,不需要考虑那么多,上来先定义一个函数,然后使用各种诸如if-else、for-each等方式进行代码执行。
+### 什么是面向过程?
-最典型的用法就是实现一个简单的算法,比如实现冒泡排序。
+面向过程(Procedure Oriented)是一种以过程为中心的编程思想,是一种自顶而下的编程模式。最典型的面向过程的编程语言就是C语言。
+简单来说,面向过程的开发范式中,程序员需要把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。
-### 什么是面向对象?
+就是说,在进行面向过程编程的时候,不需要考虑那么多,上来先定义一个函数,然后使用各种诸如if-else、for-each等方式进行代码执行。最典型的用法就是实现一个简单的算法,比如实现冒泡排序。
-#### 概述: 将事务高度抽象化的编程模式.
+面向过程进行的软件开发,其代码都是流程化的,很明确的可以看出第一步做什么、第二步做什么。这种方式的代码执行起来效率很高。
-将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。
+但是,面向过程同时存在着代码重用性低,扩展能力差,后期维护难度比较大等问题。
-就是说,在进行面向对象进行编程的时候,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。
-比如:想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car类。
+### 什么是面向对象?
+
+面向对象(Object Oriented)的雏形,最早在出现在1960年的Simula语言中,当时的程序设计领域正面临着一种危机:在软硬件环境逐渐复杂的情况下,软件如何得到良好的维护?
-### 举例说明区别
+面向对象程序设计在某种程度上通过强调可重复性解决了这一问题。目前较为流行的面向对象语言主要有Java、C#、C++、Python、Ruby、PHP等。
-同样一个象棋设计.
+简单来说,面向对象的开发范式中,程序员将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。
-面向对象:创建黑白双方的对象负责演算,棋盘的对象负责画布,规则的对象负责判断,例子可以看出,面向对象更重视不重复造轮子,即创建一次,重复使用.
+就是说,在进行面向对象进行编程的时候,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。比如:想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car类。
-面向过程:开始—黑走—棋盘—判断—白走—棋盘—判断—循环。只需要关注每一步怎么实现即可.
+面向对象的编程方法之所以更加受欢迎,是因为他更加符合人类的思维方式。这种方式编写出来的代码扩展性、可维护性都很高。
-### 优劣对比
+与其实面向对象是一种开发范式,倒不如说面向对象是一种对现实世界的理解和抽象的方法。通过对现实世界的理解和抽象,在运用封装、继承、多态等方法,通过抽象出对象的方式进行软件开发。
-面向对象:占用资源相对高,速度相对慢
+什么是封装、继承、多态?具体如何运营面向对象的方式编写代码呢?接下来我们介绍下面向对象具有三大基本特征和五大基本原则。
-面向过程:占用资源相对低,速度相对快
diff --git a/docs/basics/object-oriented/overloading-vs-overriding.md b/docs/basics/object-oriented/overloading-vs-overriding.md
index 2e08cd24..952316d6 100644
--- a/docs/basics/object-oriented/overloading-vs-overriding.md
+++ b/docs/basics/object-oriented/overloading-vs-overriding.md
@@ -1,28 +1,39 @@
-![overloading-vs-overriding][1]
-
-重载(Overloading)和重写(Overriding)是Java中两个比较重要的概念。但是对于新手来说也比较容易混淆。本文通过两个简单的例子说明了他们之间的区别。
+重载(Overloading)和重写(Overriding)是Java中两个比较重要的概念。但是对于新手来说也比较容易混淆,本文就举两个实际的例子,来说明下到底是什么是重写和重载。
## 定义
-### 重载
+首先我们分别来看一下重载和重写的定义:
+
+重载:指的是在同一个类中,多个函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
-简单说,就是函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
+重写:指的是在Java的子类与父类中有两个名称、参数列表都相同的方法的情况。由于他们具有相同的方法签名,所以子类中的新方法将覆盖父类中原有的方法。
+
+## 重载的例子
-### 重写
+ class Dog{
+ public void bark(){
+ System.out.println("woof ");
+ }
+
+ //overloading method
+ public void bark(int num){
+ for(int i=0; i 1、重载是一个编译期概念、重写是一个运行期间概念。
->
-> 2、重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法。
->
-> 3、重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法
->
-> 4、因为在编译期已经确定调用哪个方法,所以重载并不是多态。而重写是多态。重载只是一种语言特性,是一种语法规则,与多态无关,与面向对象也无关。(注:严格来说,重载是编译时多态,即静态多态。但是,Java中提到的多态,在不特别说明的情况下都指动态多态)
+1、被重载的方法必须改变参数列表;
+2、被重载的方法可以改变返回类型;
+3、被重载的方法可以改变访问修饰符;
+4、被重载的方法可以声明新的或更广的检查异常;
+5、方法能够在同一个类中或者在一个子类中被重载。
## 重写的例子
@@ -56,58 +67,21 @@
bowl
-上面的例子中,`dog`对象被定义为`Dog`类型。在编译期,编译器会检查Dog类中是否有可访问的`bark()`方法,只要其中包含`bark()`方法,那么就可以编译通过。在运行期,`Hound`对象被`new`出来,并赋值给`dog`变量,这时,JVM是明确的知道`dog`变量指向的其实是`Hound`对象的引用。所以,当`dog`调用`bark()`方法的时候,就会调用`Hound`类中定义的`bark()`方法。这就是所谓的动态多态性。
-
-### 重写的条件
-
-> 参数列表必须完全与被重写方法的相同;
->
-> 返回类型必须完全与被重写方法的返回类型相同;
->
-> 访问级别的限制性一定不能比被重写方法的强;
->
-> 访问级别的限制性可以比被重写方法的弱;
->
-> 重写方法一定不能抛出新的检查异常或比被重写的方法声明的检查异常更广泛的检查异常
->
-> 重写的方法能够抛出更少或更有限的异常(也就是说,被重写的方法声明了异常,但重写的方法可以什么也不声明)
->
-> 不能重写被标示为final的方法;
->
-> 如果不能继承一个方法,则不能重写这个方法。
-
-## 重载的例子
-
- class Dog{
- public void bark(){
- System.out.println("woof ");
- }
-
- //overloading method
- public void bark(int num){
- for(int i=0; i 被重载的方法必须改变参数列表;
->
-> 被重载的方法可以改变返回类型;
->
-> 被重载的方法可以改变访问修饰符;
->
-> 被重载的方法可以声明新的或更广的检查异常;
->
-> 方法能够在同一个类中或者在一个子类中被重载。
+在编译期,编译器会检查Dog类中是否有可访问的`bark()`方法,只要其中包含`bark()`方法,那么就可以编译通过。
-## 参考资料
+在运行期,`Hound`对象被`new`出来,并赋值给`dog`变量,这时,JVM是明确的知道`dog`变量指向的其实是`Hound`对象的引用。所以,当`dog`调用`bark()`方法的时候,就会调用`Hound`类中定义的`bark()`方法。这就是所谓的动态多态性。
-[Overriding vs. Overloading in Java][2]
+方法重写的条件需要具备以下条件和要求:
- [1]: http://www.hollischuang.com/wp-content/uploads/2016/03/overloading-vs-overriding.png
- [2]: http://www.programcreek.com/2009/02/overriding-and-overloading-in-java-with-examples/
\ No newline at end of file
+1、参数列表必须完全与被重写方法的相同;
+2、返回类型必须完全与被重写方法的返回类型相同;
+3、访问级别的限制性一定不能比被重写方法的强;
+4、访问级别的限制性可以比被重写方法的弱;
+5、重写方法一定不能抛出新的检查异常或比被重写的方法声明的检查异常更广泛的检查异常
+6、重写的方法能够抛出更少或更有限的异常(也就是说,被重写的方法声明了异常,但重写的方法可以什么也不声明)
+7、不能重写被标示为final的方法;
+8、如果不能继承一个方法,则不能重写这个方法。
\ No newline at end of file
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/polymorphism.md b/docs/basics/object-oriented/polymorphism.md
index ff84a312..8fa3608d 100644
--- a/docs/basics/object-oriented/polymorphism.md
+++ b/docs/basics/object-oriented/polymorphism.md
@@ -1,13 +1,46 @@
-### 什么是多态
+在第1.2章节中,我们介绍了面向对象的封装、继承和多态这三个基本特性,并且分别对封装和继承简单的举例做了说明。
-多态的概念呢比较简单,就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
+这一章节中,我们针对上一章节遗留的多态性进行展开介绍。
-如果按照这个概念来定义的话,那么多态应该是一种运行期的状态。
-### 多态的必要条件
-为了实现运行期的多态,或者说是动态绑定,需要满足三个条件。
+## 什么是多态
-即有类继承或者接口实现、子类要重写父类的方法、父类的引用指向子类的对象。
+我们先基于所有的编程语言介绍了什么是多态以及多态的分类。然后再重点介绍下Java中的多态。
+
+多态(Polymorphism),指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。一般情况下,可以把多态分成以下几类:
+
+* 特设多态:为个体的特定类型的任意集合定义一个共同接口。
+* 参数多态:指定一个或多个类型不靠名字而是靠可以标识任何类型的抽象符号。
+* 子类型:一个名字指称很多不同的类的实例,这些类有某个共同的超类。
+
+### 特设多态
+
+特设多态是程序设计语言的一种多态,多态函数有多个不同的实现,依赖于其实参而调用相应版本的函数。
+
+上一节我们介绍过的函数重载是特设多态的一种,除此之外还有运算符重载也是特设多态的一种。
+
+
+### 参数多态
+
+参数多态在程序设计语言与类型论中是指声明与定义函数、复合类型、变量时不指定其具体的类型,而把这部分类型作为参数使用,使得该定义对各种具体类型都适用。
+
+参数多态其实也有很广泛的应用,比如Java中的泛型就是参数多态的一种。参数多态另外一个应用比较广泛的地方就是函数式编程。
+
+### 子类型
+
+在面向对象程序设计中,计算机程序运行时,相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。
+
+这种子类型多态其实就是Java中常见的多态,下面我们针对Java中的这种子类型多态展开介绍下。
+
+## Java中的多态
+
+Java中的多态的概念比较简单,就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
+
+Java中多态其实是一种运行期的状态。为了实现运行期的多态,或者说是动态绑定,需要满足三个条件:
+
+* 有类继承或者接口实现
+* 子类要重写父类的方法
+* 父类的引用指向子类的对象
简单来一段代码解释下:
@@ -40,20 +73,28 @@
这样,就实现了多态,同样是Parent类的实例,p.call 调用的是Son类的实现、p1.call调用的是Daughter的实现。
-有人说,你自己定义的时候不就已经知道p是son,p1是Daughter了么。但是,有些时候你用到的对象并不都是自己声明的啊 。
+有人说,你自己定义的时候不就已经知道p是son,p1是Daughter了么。但是,有些时候你用到的对象并不都是自己声明的。
比如Spring 中的IOC出来的对象,你在使用的时候就不知道他是谁,或者说你可以不用关心他是谁。根据具体情况而定。
+> IOC,是Ioc—Inversion of Control 的缩写,中文翻译成“控制反转”,它是一种设计思想,意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。
+>
+> 换句话说当我们使用Spring框架的时候,对象是Spring容器创建出来并由容器进行管理,我们只需要使用就行了。
-另外,还有一种说法,包括维基百科也说明,多态还分为动态多态和静态多态。
+### 静态多态
+
+上面我们说的多态,是一种运行期的概念。另外,还有一种说法,认为多态还分为动态多态和静态多态。
上面提到的那种动态绑定认为是动态多态,因为只有在运行期才能知道真正调用的是哪个类的方法。
-还有一种静态多态,一般认为Java中的函数重载是一种静态多态,因为他需要在编译期决定具体调用哪个方法、
+很多人认为,还有一种静态多态,一般认为Java中的函数重载是一种静态多态,因为他需要在编译期决定具体调用哪个方法。
+
+结合2.1章节,我们介绍过的重载和重写的相关概念,我们再来总结下重载和重写这两个概念:
-关于这个动态静态的说法,我更偏向于重载和多态其实是无关的。
+1、重载是一个编译期概念、重写是一个运行期概念。
-但是也要看情况,普通场合,我会认为只有方法的重写算是多态,毕竟这是我的观点。但是如果在面试的时候,我“可能”会认为重载也算是多态,毕竟面试官也有他的观点。我会和面试官说:我认为,多态应该是一种运行期特性,Java中的重写是多态的体现。不过也有人提出重载是一种静态多态的想法,这个问题在StackOverflow等网站上有很多人讨论,但是并没有什么定论。我更加倾向于重载不是多态。
+2、重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法。
-这样沟通,既能体现出你了解的多,又能表现出你有自己的思维,不是那种别人说什么就是什么的。
+3、重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法。
+4、Java中的方法重写是Java多态(子类型)的实现方式。而Java中的方法重写其实是特设多态的一种实现方式。
\ No newline at end of file
diff --git a/docs/basics/object-oriented/principle.md b/docs/basics/object-oriented/principle.md
index 43e92a59..2f93e606 100644
--- a/docs/basics/object-oriented/principle.md
+++ b/docs/basics/object-oriented/principle.md
@@ -1,40 +1,66 @@
+面向对象开发范式的最大的好处就是易用、易扩展、易维护,但是,什么样的代码是易用、易扩展、易维护的呢?如何衡量他们呢?
-
+罗伯特·C·马丁在21世纪早期提出了SOLID原则,这是五个原则的缩写的组合,这五个原则沿用至今。
### 单一职责原则(Single-Responsibility Principle)
-其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。
+其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。
+
+单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。
专注,是一个人优良的品质;同样的,单一也是一个类的优良设计。交杂不清的职责将使得代码看起来特别别扭牵一发而动全身,有失美感和必然导致丑陋的系统错误风险。
### 开放封闭原则(Open-Closed principle)
-其核心思想是:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。开放封闭原则主要体现在两个方面1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。2、对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
+其核心思想是:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
+
+开放封闭原则主要体现在两个方面:
+
+1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
+
+2、对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
+
实现开放封闭原则的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。
“需求总是变化”没有不变的软件,所以就需要用封闭开放原则来封闭变化满足需求,同时还能保持软件内部的封装体系稳定,不被需求的变化影响。
-### Liskov替换原则(Liskov-Substitution Principle)
+### 里氏替换原则(Liskov-Substitution Principle)
+
+其核心思想是:子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
+
+在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
+里氏替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了Liskov替换原则,才能保证继承复用是可靠地。实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。
+
+里氏替换原则是关于继承机制的设计原则,违反了Liskov替换原则就必然导致违反开放封闭原则。
-其核心思想是:子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
-Liskov替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了Liskov替换原则,才能保证继承复用是可靠地。实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。
-Liskov替换原则是关于继承机制的设计原则,违反了Liskov替换原则就必然导致违反开放封闭原则。
-Liskov替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。
+里氏替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。
### 依赖倒置原则(Dependecy-Inversion Principle)
其核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
+
我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。
抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。
+
依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一层不变的。依赖于抽象,就是对接口编程,不要对实现编程。
### 接口隔离原则(Interface-Segregation Principle)
其核心思想是:使用多个小的专门的接口,而不要使用一个大的总接口。
+
具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。
+
接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。
-分离的手段主要有以下两种:1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。
+分离的手段主要有以下两种:
+
+1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。
+
+2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。
+
+以上就是5个基本的面向对象设计原则,它们就像面向对象程序设计中的金科玉律,遵守它们可以使我们的代码更加鲜活,易于复用,易于拓展,灵活优雅。
+
+不同的设计模式对应不同的需求,而设计原则则代表永恒的灵魂,需要在实践中时时刻刻地遵守。就如ARTHUR J.RIEL在那边《OOD启示录》中所说的:“你并不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。但你应当把这些原则看做警铃,若违背了其中的一条,那么警铃就会响起。”
-以上就是5个基本的面向对象设计原则,它们就像面向对象程序设计中的金科玉律,遵守它们可以使我们的代码更加鲜活,易于复用,易于拓展,灵活优雅。不同的设计模式对应不同的需求,而设计原则则代表永恒的灵魂,需要在实践中时时刻刻地遵守。就如ARTHUR J.RIEL在那边《OOD启示录》中所说的:“你并不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。但你应当把这些原则看做警铃,若违背了其中的一条,那么警铃就会响起。”
+很多人刚开始可能对这些原则无法深刻的理解,但是没关系,随着自己开发经验的增长,就会慢慢的可以理解这些原则了。
\ No newline at end of file
diff --git a/docs/basics/object-oriented/scope.md b/docs/basics/object-oriented/scope.md
index 39c43aad..f03889bf 100644
--- a/docs/basics/object-oriented/scope.md
+++ b/docs/basics/object-oriented/scope.md
@@ -1,3 +1,6 @@
+我们通过封装的手段,将成员变量、方法等包装在一个类中,那么,被封装在类中的这些成员变量和方法,能不能被外部访问呢?能被谁访问呢?
+
+这种能不能被访问、能被谁访问的特性,Java是通过访问控制修饰符来实现的。Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问,Java 支持 4 种不同的访问权限。
对于成员变量和方法的作用域,public,protected,private以及不写之间的区别:
@@ -8,4 +11,5 @@
`protected` : 表明成员变量或者方法对类自身,与同在一个包中的其他类可见,其他包下的类不可访问,除非是他的子类
-`default` : 表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类
\ No newline at end of file
+`default` : 表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类
+
diff --git a/docs/basics/object-oriented/variable.md b/docs/basics/object-oriented/variable.md
index cc3849c2..b2ba4b2a 100644
--- a/docs/basics/object-oriented/variable.md
+++ b/docs/basics/object-oriented/variable.md
@@ -25,3 +25,11 @@ Java中共有三种变量,分别是类变量、成员变量和局部变量。
}
```
上面定义的三个变量中,变量a就是类变量,变量b就是成员变量,而变量c和d是局部变量。
+
+a作为类变量,他存放在方法区中;b作为成员变量,和对象一起存储在堆内存中(不考虑栈上分配的情况);c和d作为方法的局部变量,保存在栈内存中。
+
+之所以要在这一章节重点介绍下这三种变量类型,是因为很多人因为不知道这三种类型的区别,所以不知道他们分别存放在哪里,这就导致不知道那些变量需要考虑并发问题。
+
+关于并发问题,目前本书《基本篇》还不涉及,会在下一本《并发篇》中重点介绍,这里先简单说明一下:
+
+因为只有共享变量才会遇到并发问题,所以,变量a和b是共享变量,变量c和d是非共享变量。所以如果遇到多线程场景,对于变量a和b的操作是需要考虑线程安全的,而对于线程c和d的操作是不需要考虑线程安全的。
\ No newline at end of file
diff --git a/docs/basics/object-oriented/why-pass-by-reference.md b/docs/basics/object-oriented/why-pass-by-reference.md
index 58a5f027..f76bb96f 100644
--- a/docs/basics/object-oriented/why-pass-by-reference.md
+++ b/docs/basics/object-oriented/why-pass-by-reference.md
@@ -1,90 +1,3 @@
-对于初学者来说,要想把这个问题回答正确,最初思考这个问题的时候,我发现我竟然无法通过简单的语言把这个事情描述的很容易理解,遗憾的是,我也没有在网上找到哪篇文章可以把这个事情讲解的通俗易懂。所以,就有了我写这篇文章的初衷。
-
-### 辟谣时间
-
-关于这个问题,在[StackOverflow][2]上也引发过广泛的讨论,看来很多程序员对于这个问题的理解都不尽相同,甚至很多人理解的是错误的。还有的人可能知道Java中的参数传递是值传递,但是说不出来为什么。
-
-在开始深入讲解之前,有必要纠正一下大家以前的那些错误看法了。如果你有以下想法,那么你有必要好好阅读本文。
-
-> 错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。
->
-> 错误理解二:Java是引用传递。
->
-> 错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。
-
-### 实参与形参
-
-我们都知道,在Java中定义方法的时候是可以定义参数的。比如Java中的main方法,`public static void main(String[] args)`,这里面的args就是参数。参数在程序语言中分为形式参数和实际参数。
-
-> 形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。
->
-> 实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。
-
-简单举个例子:
-
- public static void main(String[] args) {
- ParamTest pt = new ParamTest();
- pt.sout("Hollis");//实际参数为 Hollis
- }
-
- public void sout(String name) { //形式参数为 name
- System.out.println(name);
- }
-
-
-实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。
-
-### 求值策略
-
-我们说当进行方法调用的时候,需要把实际参数传递给形式参数,那么传递的过程中到底传递的是什么东西呢?
-
-这其实是程序设计中**求值策略(Evaluation strategies)**的概念。
-
-在计算机科学中,求值策略是确定编程语言中表达式的求值的一组(通常确定性的)规则。求值策略定义何时和以何种顺序求值给函数的实际参数、什么时候把它们代换入函数、和代换以何种形式发生。
-
-求值策略分为两大基本类,基于如何处理给函数的实际参数,分为严格的和非严格的。
-
-#### 严格求值
-
-在“严格求值”中,函数调用过程中,给函数的实际参数总是在应用这个函数之前求值。多数现存编程语言对函数都使用严格求值。所以,我们本文只关注严格求值。
-
-在严格求值中有几个关键的求值策略是我们比较关心的,那就是**传值调用**(Call by value)、**传引用调用**(Call by reference)以及**传共享对象调用**(Call by sharing)。
-
-* 传值调用(值传递)
- * 在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。因为形式参数拿到的只是一个"局部拷贝",所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。
-* 传引用调用(引用传递)
- * 在传引用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。因为传递的是引用,所以,如果在被调函数中改变了形式参数的值,改变对于调用者来说是可见的。
-* 传共享对象调用(共享对象传递)
- * 传共享对象调用中,先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。因为参数的地址都指向同一个对象,所以我们也称之为"传共享对象",所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。
-
-不知道大家有没有发现,其实传共享对象调用和传值调用的过程几乎是一样的,都是进行"求值"、"拷贝"、"传递"。你品,你细品。
-
-![][1]
-
-但是,传共享对象调用和内传引用调用的结果又是一样的,都是在被调函数中如果改变参数的内容,那么这种改变也会对调用者有影响。你再品,你再细品。
-
-那么,共享对象传递和值传递以及引用传递之间到底有很么关系呢?
-
-对于这个问题,我们应该关注过程,而不是结果,**因为传共享对象调用的过程和传值调用的过程是一样的,而且都有一步关键的操作,那就是"复制",所以,通常我们认为传共享对象调用是传值调用的特例**
-
-我们先把传共享对象调用放在一边,我们再来回顾下传值调用和传引用调用的主要区别:
-
-**传值调用是指在调用函数时将实际参数`复制`一份传递到函数中,传引用调用是指在调用函数时将实际参数的引用`直接`传递到函数中。**
-
-![pass-by-reference-vs-pass-by-value-animation][2]
-
-所以,两者的最主要区别就是是直接传递的,还是传递的是一个副本。
-
-这里我们来举一个形象的例子。再来深入理解一下传值调用和传引用调用:
-
-你有一把钥匙,当你的朋友想要去你家的时候,如果你`直接`把你的钥匙给他了,这就是引用传递。
-
-这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。
-
-你有一把钥匙,当你的朋友想要去你家的时候,你`复刻`了一把新钥匙给他,自己的还在自己手里,这就是值传递。
-
-这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。
-
### Java的求值策略
前面我们介绍过了传值调用、传引用调用以及传值调用的特例传共享对象调用,那么,Java中是采用的哪种求值策略呢?
@@ -174,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);
}
@@ -220,9 +134,7 @@ OK,以上就是本文的全部内容,不知道本文是否帮助你解开了
[Passing by Value vs. by Reference Visual Explanation][6]
- [1]: https://www.hollischuang.com/wp-content/uploads/2020/04/15865905252659.jpg
- [2]: https://www.hollischuang.com/wp-content/uploads/2020/04/pass-by-reference-vs-pass-by-value-animation.gif
[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 06ea55c7..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 | 增加新技术知识、完善知识体系 |
@@ -15,6 +19,15 @@
欢迎大家参与共建~
+### 联系我们
+
+欢迎关注作者的公众号,可以直接后台留言。
+
+
+
+*公众号后台回复:"成神导图",即可获取《Java工程师成神之路最新版思维导图》*
+
+
### 关于作者
Hollis,阿里巴巴技术专家,51CTO专栏作家,CSDN博客专家,掘金优秀作者,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者;热衷于分享计算机编程相关技术,博文全网阅读量上千万。
@@ -43,14 +56,6 @@ GitHub Pages 完整阅读:[进入](https://hollischuang.github.io/toBeTopJavae
Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer) (国内访问速度较快)
-### 联系我们
-
-欢迎关注作者的公众号,可以直接后台留言。
-
-
-
-如果获取《Java工程师成神之路最新版思维导图》,请在公众号后台回复:"成神导图"
-
### 目录
@@ -73,6 +78,8 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* [方法重写与重载](/basics/object-oriented/overloading-vs-overriding.md)
* [Java的继承与实现](/basics/object-oriented/extends-implement.md)
+
+ * [Java为什么不支持多继承](/basics/object-oriented/multiple-inheritance.md)
* [Java的继承与组合](/basics/object-oriented/inheritance-composition.md)
@@ -127,6 +134,8 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* [String对“+”的重载](/basics/java-basic/string-append.md)
* [字符串拼接的几种方式和区别](/basics/java-basic/string-concat.md)
+
+ * [Java 8中的StringJoiner](/basics/java-basic/stringjoiner-in-java8.md)
* [String.valueOf和Integer.toString的区别](/basics/java-basic/value-of-vs-to-string.md)
@@ -226,6 +235,8 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* [枚举的线程安全性问题](/basics/java-basic/enum-thread-safe.md)
+ * [为什么不建议在对外接口中使用枚举](/basics/java-basic/stop-use-enum-in-api.md)
+
* IO
* [字符流、字节流](/basics/java-basic/byte-stream-vs-character-stream.md)
@@ -312,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)
@@ -404,6 +415,8 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* [yyyy和YYYY有什么区别?](/basics/java-basic/YYYY-vs-yyyy.md)
+ * 为什么日期格式化时必须有使用y表示年,而不能用Y?
+
* 编码方式
* [什么是ASCII?](/basics/java-basic/ASCII.md)
@@ -442,6 +455,14 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* javax.management.*
+ * BigDecimal
+
+ * 为什么0.1+0.2不等于0.3
+
+ * [为什么不能使用BigDecimal的equals比较大小](/basics/java-basic/stop-using-equlas-in-bigdecimal.md)
+
+ * [为什么不能直接使用double创建一个BigDecimal](/basics/java-basic/stop-create-bigdecimal-with-double.md)
+
* Java 8
* [lambda表达式](/basics/java-basic/lambda.md)
@@ -484,29 +505,31 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 线程
- * 线程与进程的区别
+ * [线程与进程的区别](/basics/concurrent-coding/progress-vs-thread.md)
+
+ * [线程的特点](/basics/concurrent-coding/thread.md)
- * 线程的实现
+ * [线程的实现](/basics/concurrent-coding/implement-of-thread.md)
- * 线程的状态
+ * [线程的状态](/basics/concurrent-coding/state-of-thread.md)
- * 线程优先级
+ * [线程优先级](/basics/concurrent-coding/priority-of-thread.md)
- * 线程调度
+ * [线程调度](/basics/concurrent-coding/thread-scheduling.md)
- * 多线程如何Debug
+ * [多线程如何Debug](/basics/concurrent-coding/debug-in-multithread.md)
- * 守护线程
+ * [守护线程](/basics/concurrent-coding/deamon-thread.md)
* 创建线程的多种方式
- * 继承Thread类创建线程
+ * [继承Thread类创建线程](/basics/concurrent-coding/create-thread-with-extends.md)
- * 实现Runnable接口创建线程
+ * [实现Runnable接口创建线程](/basics/concurrent-coding/create-thread-with-Implement.md)
- * 通过Callable和FutureTask创建线程
+ * [通过Callable和FutureTask创建线程](/basics/concurrent-coding/create-thread-with-callback-future-task.md)
- * 通过线程池创建线程
+ * [通过线程池创建线程](/basics/concurrent-coding/create-thread-with-thead-pool.md)
* 线程池
@@ -516,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)
* 多级缓存和一致性问题
@@ -656,7 +679,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 运行时数据区
- * 运行时数据区哪些是线程独享
+ * [运行时数据区哪些是线程独享](/basement/jvm/exclusive-in-runtime-area.md)
* 堆和栈区别
@@ -772,9 +795,27 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 类加载过程
- * 双亲委派(破坏双亲委派)
+ * 如何判断JVM中类和其他类是不是同一个类
+
+ * [双亲委派原则](/basement/jvm/parents-delegate.md)
+
+ * [为什么需要双亲委派?](/basement/jvm/why-parents-delegate.md)
+
+ * [“父子加载器”之间的关系是继承吗?](/basement/jvm/relation-with-parents-delegate.md)
+
+ * [双亲委派是如何实现的?](/basement/jvm/implements-of-parents-delegate.md)
+
+ * [如何打破双亲委派](/basement/jvm/ibreak-parants-delegate.md)
+
+ * [如何自定义类加载器](/basement/jvm/define-class-loader.md)
+
+ * [双亲委派被破坏的例子](/basement/jvm/sample-of-break-parents-delegate.md)
+
+ * [为什么JNDI,JDBC等需要破坏双亲委派?](/basement/jvm/spi-parents-delegate.md)
+
+ * [为什么Tomcat要破坏双亲委派](/basement/jvm/tomcat-parents-delegate.md)
- * 模块化(jboss modules、osgi、jigsaw)
+ * [模块化(jboss modules、osgi、jigsaw)](/basement/jvm/moduler.md)
* 打包工具
@@ -966,7 +1007,11 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 用Java写一个简单的静态文件的HTTP服务器
- * http/2
+ * HTTP/2
+
+ * HTTP/2 存在哪些问题?
+
+ * HTTP/3
* Java RMI,Socket,HttpClient
@@ -1044,10 +1089,14 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* AOP原理
+ * Spring AOP不支持方法自调用的问题
+
* 实现Spring的IOC
* spring四种依赖注入方式
+ * 为什么我不建议使用@Transactional声明事务
+
* Spring MVC
* 什么是MVC
@@ -1305,6 +1354,8 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 为什么kill -9 不能随便执行
+ * rm一个被打开的文件会发生什么
+
* 进程间通信
* 服务器性能指标
@@ -1431,6 +1482,10 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* memcached
+ * Redis
+
+ * Redis多线程
+
* 分别使用数据库锁、NoSql实现分布式锁
* 性能调优
@@ -1773,6 +1828,12 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
* 主主复制
* 异地多活
+
+ * 预案
+
+ * 预热
+
+ * 限流
* 高性能
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