diff --git a/.gitignore b/.gitignore
index 5927d83f..3587059e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,3 +57,4 @@ fabric.properties
# Editor-based Rest Client
.idea/httpRequests
+.DS_Store
\ No newline at end of file
diff --git a/README.md b/README.md
index 38c77f05..acf8a187 100644
--- a/README.md
+++ b/README.md
@@ -3,18 +3,36 @@
  
+
| 主要版本 | 更新时间 | 备注 |
| ---- | ---------- | -------------- |
+| 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 | 增加新技术知识、完善知识体系 |
| v1.0 | 2015-08-01 | 首次发布 |
+Java成神之路全套面试题——围绕成神之路,500多道题,60多万字>>>
+
+
+
+
+扫码下单后,按照短信提示操作即可。
+
目前正在更新中...
欢迎大家参与共建~
+### 联系我们
+
+欢迎关注作者的公众号,可以直接后台留言。
+
+
+
+*公众号后台回复:"成神导图",即可获取《Java工程师成神之路最新版思维导图》*
+
+
### 在线阅读地址
GitHub Pages 完整阅读:[进入](https://hollischuang.github.io/toBeTopJavaer/)
@@ -24,7 +42,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
### 关于作者
-Hollis,阿里巴巴技术专家,51CTO专栏作家,CSDN博客专家,掘金优秀作者,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者;热衷于分享计算机编程相关技术,博文全网阅读量上千万。
+Hollis,阿里巴巴技术专家,51CTO专栏作家,CSDN博客专家,掘金优秀作者,《深入理解Java核心技术》作者,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者;热衷于分享计算机编程相关技术,博文全网阅读量上千万。
### 开源协议
@@ -43,12 +61,3 @@ Hollis,阿里巴巴技术专家,51CTO专栏作家,CSDN博客专家,掘
请直接在[GitHub](https://github.com/hollischuang/toBeTopJavaer)上以issue或者PR的形式提出
如果本项目中的内容侵犯了您的任何权益,欢迎通过邮箱(hollischuang@gmail)与我联系
-
-
-### 联系我们
-
-欢迎关注作者的公众号,可以直接后台留言。
-
-
-
-如果获取《Java工程师成神之路最新版思维导图》,请在公众号后台回复:"成神导图"
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index 7d6b89ff..45ea4c4a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -2,9 +2,13 @@
  
+成神之路系列丛书的第一本《深入理解Java核心技术(基础篇)》已经正式出版了,这本书囊括了
调整部分知识的顺序及结构,方便阅读和理解
通过GitHub Page搭建,便于阅读|
| v2.0 | 2019-02-19 | 结构调整,更适合从入门到精通;
进一步完善知识体系;
新技术补充;|
| v1.1 | 2018-03-12 | 增加新技术知识、完善知识体系 |
@@ -15,6 +19,14 @@
欢迎大家参与共建~
+### 联系我们
+
+欢迎关注作者的公众号,可以直接后台留言。
+
+
+
+*公众号后台回复:"成神导图",即可获取《Java工程师成神之路最新版思维导图》*
+
### 关于作者
Hollis,阿里巴巴技术专家,51CTO专栏作家,CSDN博客专家,掘金优秀作者,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者;热衷于分享计算机编程相关技术,博文全网阅读量上千万。
@@ -43,12 +55,6 @@ GitHub Pages 完整阅读:[进入](https://hollischuang.github.io/toBeTopJavae
Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer) (国内访问速度较快)
-### 联系我们
-
-欢迎关注作者的公众号,可以直接后台留言。
-
-
-如果获取《Java工程师成神之路最新版思维导图》,请在公众号后台回复:"成神导图"
### 开始阅读
][6]
diff --git a/docs/advance/design-patterns/adapter-pattern.md b/docs/advance/design-patterns/adapter-pattern.md
index b5db9af9..49384606 100644
--- a/docs/advance/design-patterns/adapter-pattern.md
+++ b/docs/advance/design-patterns/adapter-pattern.md
@@ -125,7 +125,7 @@ GOF中将适配器模式分为类适配器模式和对象适配器模式。区
public void charge(){
System.out.println("开始给我的GalaxyS7手机充电...");
microUsbInterface.chargeWithMicroUsb();
- System.out.println("开始给我的GalaxyS7手机充电...");
+ System.out.println("结束给我的GalaxyS7手机充电...");
}
public MicroUsbInterface getMicroUsbInterface() {
@@ -203,14 +203,14 @@ GOF中将适配器模式分为类适配器模式和对象适配器模式。区
==============================
开始给我的GalaxyS7手机充电...
使用MicroUsb型号的充电器充电...
- 开始给我的GalaxyS7手机充电...
+ 结束给我的GalaxyS7手机充电...
==============================
开始给我的Iphone6Plus手机充电...
使用MicroUsb型号的充电器充电...
结束给我的Iphone6Plus手机充电...
-上面的例子通过适配器,把一个MicroUsb型号的充电器用来给Iphone充电。从代码层面,就是通过适配器复用了MicroUsb接口及其实现类。在很大程度上福永了已有的代码。
+上面的例子通过适配器,把一个MicroUsb型号的充电器用来给Iphone充电。从代码层面,就是通过适配器复用了MicroUsb接口及其实现类。在很大程度上复用了已有的代码。
## 优缺点
diff --git a/docs/advance/design-patterns/strategy-pattern.md b/docs/advance/design-patterns/strategy-pattern.md
index 29f26245..7ef203f8 100644
--- a/docs/advance/design-patterns/strategy-pattern.md
+++ b/docs/advance/design-patterns/strategy-pattern.md
@@ -88,7 +88,7 @@
@Override
public double calPrice(double bookPrice) {
- System.out.println("对于中级会员的折扣为20%");
+ System.out.println("对于高级会员的折扣为20%");
return bookPrice * 0.8;
}
}
@@ -149,7 +149,7 @@
}
}
- //对于中级会员的折扣为20%
+ //对于高级会员的折扣为20%
//高级会员图书的最终价格为:240.0
//对于中级会员的折扣为10%
//中级会员图书的最终价格为:270.0
diff --git a/docs/basement/jvm/break-parants-delegate.md b/docs/basement/jvm/break-parants-delegate.md
new file mode 100644
index 00000000..4a31d080
--- /dev/null
+++ b/docs/basement/jvm/break-parants-delegate.md
@@ -0,0 +1,3 @@
+知道了双亲委派模型的实现,那么想要破坏双亲委派机制就很简单了。
+
+因为他的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。
\ No newline at end of file
diff --git a/docs/basement/jvm/define-class-loader.md b/docs/basement/jvm/define-class-loader.md
new file mode 100644
index 00000000..d0133017
--- /dev/null
+++ b/docs/basement/jvm/define-class-loader.md
@@ -0,0 +1,29 @@
+ClassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClass和defineClass等,那么这几个方法有什么区别呢?
+
+* loadClass()
+ * 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
+* findClass()
+ * 根据名称或位置加载.class字节码
+* definclass()
+ * 把字节码转化为Class
+
+这里面需要展开讲一下loadClass和findClass,我们前面说过,当我们想要自定义一个类加载器的时候,并且像破坏双亲委派原则时,我们会重写loadClass方法。
+
+那么,如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?
+
+这时候,就可以继承ClassLoader,并且重写findClass方法。findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法。
+
+ /**
+ * @since 1.2
+ */
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ throw new ClassNotFoundException(name);
+ }
+
+这个方法只抛出了一个异常,没有默认实现。
+
+JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中。
+
+因为在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载。
+
+所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。
\ No newline at end of file
diff --git a/docs/basement/jvm/exclusive-in-runtime-area.md b/docs/basement/jvm/exclusive-in-runtime-area.md
new file mode 100644
index 00000000..b0332c5e
--- /dev/null
+++ b/docs/basement/jvm/exclusive-in-runtime-area.md
@@ -0,0 +1,3 @@
+在JVM运行时内存区域中,PC寄存器、虚拟机栈和本地方法栈是线程独享的。
+
+而Java堆、方法区是线程共享的。但是值得注意的是,Java堆其实还未每一个线程单独分配了一块TLAB空间,这部分空间在分配时是线程独享的,在使用时是线程共享的。
\ No newline at end of file
diff --git a/docs/basement/jvm/implements-of-parents-delegate.md b/docs/basement/jvm/implements-of-parents-delegate.md
new file mode 100644
index 00000000..d46750e6
--- /dev/null
+++ b/docs/basement/jvm/implements-of-parents-delegate.md
@@ -0,0 +1,51 @@
+双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现并不复杂。
+
+实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中:
+
+ protected Class> loadClass(String name, boolean resolve)
+ throws ClassNotFoundException
+ {
+ synchronized (getClassLoadingLock(name)) {
+ // First, check if the class has already been loaded
+ Class> c = findLoadedClass(name);
+ if (c == null) {
+ long t0 = System.nanoTime();
+ try {
+ if (parent != null) {
+ c = parent.loadClass(name, false);
+ } else {
+ c = findBootstrapClassOrNull(name);
+ }
+ } catch (ClassNotFoundException e) {
+ // ClassNotFoundException thrown if class not found
+ // from the non-null parent class loader
+ }
+
+ if (c == null) {
+ // If still not found, then invoke findClass in order
+ // to find the class.
+ long t1 = System.nanoTime();
+ c = findClass(name);
+
+ // this is the defining class loader; record the stats
+ sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
+ sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
+ sun.misc.PerfCounter.getFindClasses().increment();
+ }
+ }
+ if (resolve) {
+ resolveClass(c);
+ }
+ return c;
+ }
+ }
+
+代码不难理解,主要就是以下几个步骤:
+
+1、先检查类是否已经被加载过
+
+2、若没有加载则调用父加载器的loadClass()方法进行加载
+
+3、若父加载器为空则默认使用启动类加载器作为父加载器。
+
+4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
\ No newline at end of file
diff --git a/docs/basement/jvm/moduler.md b/docs/basement/jvm/moduler.md
new file mode 100644
index 00000000..11eea7b7
--- /dev/null
+++ b/docs/basement/jvm/moduler.md
@@ -0,0 +1,39 @@
+近几年模块化技术已经很成熟了,在JDK 9中已经应用了模块化的技术。
+
+其实早在JDK 9之前,OSGI这种框架已经是模块化的了,**而OSGI之所以能够实现模块热插拔和模块内部可见性的精准控制都归结于其特殊的类加载机制,加载器之间的关系不再是双亲委派模型的树状结构,而是发展成复杂的网状结构。**
+
+
+
+**在JDK中,双亲委派也不是绝对的了。**
+
+在JDK9之前,JVM的基础类以前都是在rt.jar这个包里,这个包也是JRE运行的基石。
+
+这不仅是违反了单一职责原则,同样程序在编译的时候会将很多无用的类也一并打包,造成臃肿。
+
+**在JDK9中,整个JDK都基于模块化进行构建,以前的rt.jar, tool.jar被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。**
+
+ Class> c = findLoadedClass(cn);
+ if (c == null) {
+ // 找到当前类属于哪个模块
+ LoadedModule loadedModule = findLoadedModule(cn);
+ if (loadedModule != null) {
+ //获取当前模块的类加载器
+ BuiltinClassLoader loader = loadedModule.loader();
+ //进行类加载
+ c = findClassInModuleOrNull(loadedModule, cn);
+ } else {
+ // 找不到模块信息才会进行双亲委派
+ if (parent != null) {
+ c = parent.loadClassOrNull(cn);
+ }
+ }
+ }
+
+
+### 总结
+
+以上,从什么是双亲委派,到如何实现与破坏双亲委派,又从破坏双亲委派的示例等多个方面全面介绍了关于双亲委派的知识。
+
+相信通过学习本文,你一定对双亲委派机制有了更加深刻的了解。
+
+阅读过本文之后,反手在简历上写下:熟悉Java的类加载机制,不服来问!
\ No newline at end of file
diff --git a/docs/basement/jvm/parents-delegate.md b/docs/basement/jvm/parents-delegate.md
new file mode 100644
index 00000000..876a66e2
--- /dev/null
+++ b/docs/basement/jvm/parents-delegate.md
@@ -0,0 +1,30 @@
+虚拟机在加载类的过程中需要使用类加载器进行加载,而在Java中,类加载器有很多,那么当JVM想要加载一个.class文件的时候,到底应该由哪个类加载器加载呢?
+
+这就不得不提到”双亲委派机制”。
+
+首先,我们需要知道的是,Java语言系统中支持以下4种类加载器:
+
+
+* Bootstrap ClassLoader 启动类加载器
+* Extention ClassLoader 标准扩展类加载器
+* Application ClassLoader 应用类加载器
+* User ClassLoader 用户自定义类加载器
+
+这四种类加载器之间,是存在着一种层次关系的,如下图
+
+
+
+一般认为上一层加载器是下一层加载器的父加载器,那么,除了BootstrapClassLoader之外,所有的加载器都是有父加载器的。
+
+那么,所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
+
+那么,什么情况下父加载器会无法加载某一个类呢?
+
+其实,Java中提供的这四种类型的加载器,是有各自的职责的:
+
+* Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
+* Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
+* Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
+* User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件
+
+那么也就是说,一个用户自定义的类,如com.hollis.ClassHollis 是无论如何也不会被Bootstrap和Extention加载器加载的。
\ No newline at end of file
diff --git a/docs/basement/jvm/relation-with-parents-delegate.md b/docs/basement/jvm/relation-with-parents-delegate.md
new file mode 100644
index 00000000..dd71242e
--- /dev/null
+++ b/docs/basement/jvm/relation-with-parents-delegate.md
@@ -0,0 +1,12 @@
+很多人看到父加载器、子加载器这样的名字,就会认为Java中的类加载器之间存在着继承关系。
+
+甚至网上很多文章也会有类似的错误观点。
+
+这里需要明确一下,双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的。
+
+如下为ClassLoader中父加载器的定义:
+
+ public abstract class ClassLoader {
+ // The parent class loader for delegation
+ private final ClassLoader parent;
+ }
\ No newline at end of file
diff --git a/docs/basement/jvm/runtime-area.md b/docs/basement/jvm/runtime-area.md
new file mode 100644
index 00000000..69a2115a
--- /dev/null
+++ b/docs/basement/jvm/runtime-area.md
@@ -0,0 +1,25 @@
+Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域。《Java虚拟机规范》中规定了JVM所管理的内存需要包括一下几个运行时区域:
+
+
+
+主要包含了PC寄存器(程序计数器)、Java虚拟机栈、本地方法栈、Java堆、方法区以及运行时常量池。
+
+
+
+1、以上是Java虚拟机规范,不同的虚拟机实现会各有不同,但是一般会遵守规范。
+
+2、规范中定义的方法区,只是一种概念上的区域,并说明了其应该具有什么功能。但是并没有规定这个区域到底应该处于何处。所以,对于不同的虚拟机实现来说,是由一定的自由度的。
+
+3、不同版本的方法区所处位置不同,上图中划分的是逻辑区域,并不是绝对意义上的物理区域。因为某些版本的JDK中方法区其实是在堆中实现的。
+
+4、运行时常量池用于存放编译期生成的各种字面量和符号应用。但是,Java语言并不要求常量只有在编译期才能产生。比如在运行期,String.intern也会把新的常量放入池中。
+
+5、除了以上介绍的JVM运行时内存外,还有一块内存区域可供使用,那就是直接内存。Java虚拟机规范并没有定义这块内存区域,所以他并不由JVM管理,是利用本地方法库直接在堆外申请的内存区域。
+
+6、堆和栈的数据划分也不是绝对的,如HotSpot的JIT会针对对象分配做相应的优化。
+
+如上,做个总结,JVM内存结构,由Java虚拟机规范定义。描述的是Java程序执行过程中,由JVM管理的不同数据区域。各个区域有其特定的功能。
+
+
+但是,需要注意的是,上面的区域划分只是逻辑区域,对于有些区域的限制是比较松的,所以不同的虚拟机厂商在实现上,甚至是同一款虚拟机的不同版本也是不尽相同的。
+
diff --git a/docs/basement/jvm/sample-of-break-parents-delegate.md b/docs/basement/jvm/sample-of-break-parents-delegate.md
new file mode 100644
index 00000000..ab9f627d
--- /dev/null
+++ b/docs/basement/jvm/sample-of-break-parents-delegate.md
@@ -0,0 +1,14 @@
+双亲委派机制的破坏不是什么稀奇的事情,很多框架、容器等都会破坏这种机制来实现某些功能。
+
+*第一种被破坏的情况是在双亲委派出现之前。*
+
+由于双亲委派模型是在JDK1.2之后才被引入的,而在这之前已经有用户自定义类加载器在用了。所以,这些是没有遵守双亲委派原则的。
+
+*第二种,是JNDI、JDBC等需要加载SPI接口实现类的情况。*
+
+*第三种是为了实现热插拔热部署工具。*为了让代码动态生效而无需重启,实现方式时把模块连同类加载器一起换掉就实现了代码的热替换。
+
+*第四种时tomcat等web容器的出现。*
+
+*第五种时OSGI、Jigsaw等模块化技术的应用。*
+
diff --git a/docs/basement/jvm/spi-parents-delegate.md b/docs/basement/jvm/spi-parents-delegate.md
new file mode 100644
index 00000000..b620a1f0
--- /dev/null
+++ b/docs/basement/jvm/spi-parents-delegate.md
@@ -0,0 +1,35 @@
+我们日常开发中,大多数时候会通过API的方式调用Java提供的那些基础类,这些基础类时被Bootstrap加载的。
+
+但是,调用方式除了API之外,还有一种SPI的方式。
+
+如典型的JDBC服务,我们通常通过以下方式创建数据库连接:
+
+ Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");
+
+
+在以上代码执行之前,DriverManager会先被类加载器加载,因为java.sql.DriverManager类是位于rt.jar下面的 ,所以他会被根加载器加载。
+
+类加载时,会执行该类的静态方法。其中有一段关键的代码是:
+
+ ServiceLoader
+
+
+可以看到,图中的各个状态之间的流转路径上都有标注对应的Java中的方法。这些就是Java中进行线程调度的一些api。
+
diff --git a/docs/basics/concurrent-coding/thread-safe.md b/docs/basics/concurrent-coding/thread-safe.md
new file mode 100644
index 00000000..10374e58
--- /dev/null
+++ b/docs/basics/concurrent-coding/thread-safe.md
@@ -0,0 +1,119 @@
+# 什么是线程安全
+
+线程安全,维基百科中的解释是:
+
+> 线程安全是编程中的术语,指某个函数、函数库在**并发**环境中被调用时,能够正确地处理**多个线程**之间的**共享变量**,使程序功能正确完成。
+
+我们把这个定义拆解一下,我们需要弄清楚这么几点: 1、并发 2、多线程 3、共享变量
+
+# 并发
+
+提到线程安全,必须要提及的一个词那就是并发,如果没有并发的话,那么也就不存在线程安全问题了。
+
+## 什么是并发
+
+并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
+
+那么,操作系统视如何实现这种并发的呢?
+
+现在我们用到操作系统,无论是Windows、Linux还是MacOS等其实都是**多用户多任务分时操作系统**。使用这些操作系统的用户是可以“同时”干多件事的。
+
+但是实际上,对于单CPU的计算机来说,在CPU中,同一时间是只能干一件事儿的。为了看起来像是“同时干多件事”,分时操作系统是把CPU的时间划分成长短基本相同的时间区间,即”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个用户使用。
+
+如果某个作业在时间片结束之前,整个任务还没有完成,那么该作业就被暂停下来,放弃CPU,等待下一轮循环再继续做.此时CPU又分配给另一个作业去使用。
+
+由于计算机的处理速度很快,只要时间片的间隔取得适当,那么一个用户作业从用完分配给它的一个时间片到获得下一个CPU时间片,中间有所”停顿”,但用户察觉不出来,好像整个系统全由它”独占”似的。
+
+所以,在单CPU的计算机中,我们看起来“同时干多件事”,其实是通过CPU时间片技术,并发完成的。
+
+提到并发,还有另外一个词容易和他混淆,那就是并行。
+
+## 并发与并行之间的关系
+
+并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
+
+Erlang 之父 Joe Armstrong 用一张比较形象的图解释了并发与并行的区别:
+
+
+
+并发是两个队伍交替使用一台咖啡机。并行是两个队伍同时使用两台咖啡机。
+
+映射到计算机系统中,上图中的咖啡机就是CPU,两个队伍指的就是两个进程。
+
+# 多线程
+
+## 进程和线程
+
+理解了并发和并行之间的关系和区别后,我们再回到前面介绍的多任务分时操作系统,看看CPU是如何进行进程调度的。
+
+为了看起来像是“同时干多件事”,分时操作系统是把CPU的时间划分成长短基本相同的”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个用户的各个任务使用。
+
+在多任务处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。**上下文切换**就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成切换操作。
+
+> 在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换帧”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。
+
+对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
+
+而在多个进程之间切换的时候,需要进行上下文切换。但是上下文切换势必会耗费一些资源。于是人们考虑,能不能在一个进程中增加一些“子任务”,这样减少上下文切换的成本。比如我们使用Word的时候,它可以同时进行打字、拼写检查、字数统计等,这些子任务之间共用同一个进程资源,但是他们之间的切换不需要进行上下文切换。
+
+在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
+
+随着时间的慢慢发展,人们进一步的切分了进程和线程之间的职责。**把进程当做资源分配的基本单元,把线程当做执行的基本单元,同一个进程的多个线程之间共享资源**
+
+拿我们比较熟悉的Java语言来说,Java程序是运行在JVM上面的,每一个JVM其实就是一个进程。所有的资源分配都是基于JVM进程来的。而在这个JVM进程中,又可以创建出很多线程,多个线程之间共享JVM资源,并且多个线程可以并发执行。
+
+# 共享变量
+
+所谓共享变量,指的是多个线程都可以操作的变量。
+
+前面我们提到过,进程视分配资源的基本单位,线程是执行的基本单位。所以,多个线程之间是可以共享一部分进程中的数据的。在JVM中,Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可以操作保存在堆或者方法区中的同一个数据。那么,换句话说,保存在堆和方法区中的变量就是Java中的共享变量。
+
+那么,Java中哪些变量是存放在堆中,哪些变量是存放在方法区中,又有哪些变量是存放在栈中的呢?
+
+## 类变量、成员变量和局部变量
+
+Java中共有三种变量,分别是类变量、成员变量和局部变量。他们分别存放在JVM的方法区、堆内存和栈内存中。
+
+ /**
+ * @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是局部变量。
+
+所以,变量a和b是共享变量,变量c和d是非共享变量。所以如果遇到多线程场景,对于变量a和b的操作是需要考虑线程安全的,而对于线程c和d的操作是不需要考虑线程安全的。
+
+# 小结
+
+在了解了一些基础知识以后,我们再来回过头看看线程安全的定义:
+
+> 线程安全是编程中的术语,指某个函数、函数库在**并发**环境中被调用时,能够正确地处理**多个线程**之间的**共享变量**,使程序功能正确完成。
+
+现在我们知道了什么是并发环境,什么是多个线程以及什么是共享变量。那么只要我们在编写多线程的代码的时候注意一下,保证程序功能可以正确的执行就行了。
+
+那么问题来了,定义中说线程安全能够**正确地处理**多个线程之间的共享变量,使程序功能**正确完成**。
+
+多线程场景中存在哪些问题会导致无法正确的处理共享变量? 多线程场景中存在哪些问题会导致程序无法正确完成? 如何解决多线程场景中影响『正确』的这些问题? 解决这些问题的各个手段的实现原理又是什么?
+
+ [1]: http://www.hollischuang.com/archives/3029
+ [2]: http://www.hollischuang.com/archives/tag/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B
\ No newline at end of file
diff --git a/docs/basics/concurrent-coding/thread-scheduling.md b/docs/basics/concurrent-coding/thread-scheduling.md
new file mode 100644
index 00000000..530ee49c
--- /dev/null
+++ b/docs/basics/concurrent-coding/thread-scheduling.md
@@ -0,0 +1,59 @@
+在关于线程安全的文章中,我们提到过,对于单CPU的计算机来说,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。
+
+所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。
+
+前面关于线程状态的介绍中,我们知道,线程的运行状态中包含两种子状态,即就绪(READY)和运行中(RUNNING)。
+
+而一个线程想要从就绪状态变成运行中状态,这个过程需要系统调度,即给线程分配CPU的使用权,获得CPU使用权的线程才会从就绪状态变成运行状态。
+
+**给多个线程按照特定的机制分配CPU的使用权的过程就叫做线程调度。**
+
+还记得在介绍进程和线程的区别的时候,我们提到过的一句话吗:进程是分配资源的基本单元,线程是CPU调度的基本单元。这里所说的调度指的就是给其分配CPU时间片,让其执行任务。
+
+## Linux线程调度
+
+在Linux中,线程是由进程来实现,线程就是轻量级进程( lightweight process ),因此在Linux中,线程的调度是按照进程的调度方式来进行调度的,也就是说线程是调度单元。
+
+Linux这样实现的线程的好处的之一是:线程调度直接使用进程调度就可以了,没必要再搞一个进程内的线程调度器。在Linux中,调度器是基于线程的调度策略(scheduling policy)和静态调度优先级(static scheduling priority)来决定那个线程来运行。
+
+在Linux中,主要有三种调度策略。分别是:
+
+* SCHED_OTHER 分时调度策略,(默认的)
+* SCHED_FIFO 实时调度策略,先到先服务
+* SCHED_RR 实时调度策略,时间片轮转
+
+## Windows线程调度
+
+Windows 采用基于优先级的、抢占调度算法来调度线程。
+
+用于处理调度的 Windows 内核部分称为调度程序,Windows 调度程序确保具有最高优先级的线程总是在运行的。由于调度程序选择运行的线程会一直运行,直到被更高优先级的线程所抢占,或终止,或时间片已到,或调用阻塞系统调用(如 I/O)。如果在低优先级线程运行时,更高优先级的实时线程变成就绪,那么低优先级线程就被抢占。这种抢占使得实时线程在需要使用 CPU 时优先得到使用。
+
+# Java线程调度
+
+可以看到,不同的操作系统,有不同的线程调度策略。但是,作为一个Java开发人员来说,我们日常开发过程中一般很少关注操作系统层面的东西。
+
+主要是因为Java程序都是运行在Java虚拟机上面的,而虚拟机帮我们屏蔽了操作系统的差异,所以我们说Java是一个跨平台语言。
+
+**在操作系统中,一个Java程序其实就是一个进程。所以,我们说Java是单进程、多线程的!**
+
+前面关于线程的实现也介绍过,Thread类与大部分的Java API有显著的差别,它的所有关键方法都是声明为Native的,也就是说,他需要根据不同的操作系统有不同的实现。
+
+在Java的多线程程序中,为保证所有线程的执行能按照一定的规则执行,JVM实现了一个线程调度器,它定义了线程调度模型,对于CPU运算的分配都进行了规定,按照这些特定的机制为多个线程分配CPU的使用权。
+
+主要有两种调度模型:**协同式线程调度**和**抢占式调度模型**。
+
+## 协同式线程调度
+
+协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。协同式多线程的最大好处是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。
+
+## 抢占式调度模型
+
+抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题。
+
+系统会让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
+
+**Java虚拟机采用抢占式调度模型。**
+
+虽然Java线程调度是系统自动完成的,但是我们还是可以“建议”系统给某些线程多分配一点执行时间,另外的一些线程则可以少分配一点——这项操作可以通过设置线程优先级来完成。Java语言一共设置了10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。
+
+不过,线程优先级并不是太靠谱,原因是Java的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统,虽然现在很多操作系统都提供线程优先级的概念,但是并不见得能与Java线程的优先级一一对应。
\ No newline at end of file
diff --git a/docs/basics/concurrent-coding/thread.md b/docs/basics/concurrent-coding/thread.md
new file mode 100644
index 00000000..c943f7f6
--- /dev/null
+++ b/docs/basics/concurrent-coding/thread.md
@@ -0,0 +1,17 @@
+在多线程操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
+
+## 轻型实体
+
+线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。 线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息: (1)线程状态。 (2)当线程不运行时,被保存的现场资源。 (3)一组执行堆栈。 (4)存放每个线程的局部变量主存区。 (5)访问同一个进程中的主存和其它资源。 用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
+
+## 独立调度和分派的基本单位。
+
+在多线程操作系统中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
+
+## 可并发执行。
+
+在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
+
+## 共享进程资源。
+
+在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
diff --git a/docs/basics/concurrent-coding/volatile.md b/docs/basics/concurrent-coding/volatile.md
index 6bf347f7..1bb32ce9 100644
--- a/docs/basics/concurrent-coding/volatile.md
+++ b/docs/basics/concurrent-coding/volatile.md
@@ -24,7 +24,7 @@
return singleton;
}
}
-
+
如以上代码,是一个比较典型的使用双重锁校验的形式实现单例的,其中使用`volatile`关键字修饰可能被多个线程同时访问到的singleton。
@@ -44,9 +44,9 @@
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
-我们在[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中分析过:Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
+我们在[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中分析过:Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
-前面的关于`volatile`的原理中介绍过了,Java中的`volatile`关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用`volatile`来保证多线程操作时变量的可见性。
+前面的关于`volatile`的原理中介绍过了,Java中的`volatile`关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用`volatile`来保证多线程操作时变量的可见性。
### volatile与有序性
@@ -103,7 +103,7 @@ volatile可以禁止指令重排,这就保证了代码的程序会严格按照
System.out.println(test.inc);
}
}
-
+
以上代码比较简单,就是创建10个线程,然后分别执行1000次`i++`操作。正常情况下,程序的输出结果应该是10000,但是,多次执行的结果都小于10000。这其实就是`volatile`无法满足原子性的原因。
@@ -129,10 +129,10 @@ volatile可以禁止指令重排,这就保证了代码的程序会严格按照
return singleton;
}
}
-
+
答案,我们在下一篇文章:既生synchronized,何生亮volatile中介绍,敬请关注我的博客(http://47.103.216.138)和公众号(Hollis)。
- [1]: http://47.103.216.138/archives/2550
- [2]: http://47.103.216.138/archives/2637
- [3]: http://47.103.216.138/archives/2618
\ No newline at end of file
+[1]: http://47.103.216.138/archives/2550
+[2]: http://47.103.216.138/archives/2637
+[3]: http://47.103.216.138/archives/2618
\ No newline at end of file
diff --git a/docs/basics/concurrent-coding/why-not-executors.md b/docs/basics/concurrent-coding/why-not-executors.md
new file mode 100644
index 00000000..9460028a
--- /dev/null
+++ b/docs/basics/concurrent-coding/why-not-executors.md
@@ -0,0 +1,153 @@
+在《[深入源码分析Java线程池的实现原理][1]》这篇文章中,我们介绍过了Java中线程池的常见用法以及基本原理。
+
+在文中有这样一段描述:
+
+> 可以通过Executors静态工厂构建线程池,但一般不建议这样使用。
+
+关于这个问题,在那篇文章中并没有深入的展开。作者之所以这么说,是因为这种创建线程池的方式有很大的隐患,稍有不慎就有可能导致线上故障,如:一次Java线程池误用引发的血案和总结( 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的父类
泛型类型必须⽤限定内的类型来进⾏初始化,否则会导致编译错误。
-`⾮限定通配符`表⽰可以⽤任意泛型类型来替代,类型为
@@ -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
new file mode 100644
index 00000000..54307131
--- /dev/null
+++ b/docs/basics/java-basic/ioc-implement-with-factory-and-reflection.md
@@ -0,0 +1,195 @@
+本文系转载,原文地址:https://blog.csdn.net/fuzhongmin05/article/details/61614873
+
+### 反射机制概念
+
+我们考虑一个场景,如果我们在程序运行时,一个对象想要检视自己所拥有的成员属性,该如何操作?再考虑另一个场景,如果我们想要在运行期获得某个类的Class信息如它的属性、构造方法、一般方法后再考虑是否创建它的对象,这种情况该怎么办呢?这就需要用到反射!
+
+我们.java文件在编译后会变成.class文件,这就像是个镜面,本身是.java,在镜中是.class,他们其实是一样的;那么同理,我们看到镜子的反射是.class,就能通过反编译,了解到.java文件的本来面目。
+
+对于反射,官方给出的概念:反射是Java语言的一个特性,它允许程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作。例如它允许一个Java类获取它所有的成员变量和方法并且显示出来。
+
+反射主要是指程序可以访问,检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。在Java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。
+
+反射是Java中一种强大的工具,能够使我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码链接。但是反射使用不当会成本很高!类中有什么信息,利用反射机制就能可以获得什么信息,不过前提是得知道类的名字。
+
+### 反射机制的作用
+
+1、在运行时判断任意一个对象所属的类;
+
+2、在运行时获取类的对象;
+
+3、在运行时访问java对象的属性,方法,构造方法等。
+
+首先要搞清楚为什么要用反射机制?直接创建对象不就可以了吗,这就涉及到了动态与静态的概念。
+
+静态编译:在编译时确定类型,绑定对象,即通过。
+
+动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了Java的灵活性,体现了多态的应用,用以降低类之间的藕合性。
+
+### 反射机制的优缺点
+
+反射机制的优点:可以实现动态创建对象和编译,体现出很大的灵活性(特别是在J2EE的开发中它的灵活性就表现的十分明显)。通过反射机制我们可以获得类的各种内容,进行反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象。
+
+比如,一个大型的软件,不可能一次就把它设计得很完美,把这个程序编译后,发布了,当发现需要更新某些功能时,我们不可能要用户把以前的卸载,再重新安装新的版本,假如这样的话,这个软件肯定是没有多少人用的。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用卸载,只需要在运行时动态地创建和编译,就可以实现该功能。
+
+反射机制的缺点:对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且让它满足我们的要求。这类操作总是慢于直接执行相同的操作。
+
+### 反射与工厂模式实现IOC
+
+Spring中的IoC的实现原理就是工厂模式加反射机制。 我们首先看一下不用反射机制时的工厂模式:
+
+ interface fruit{
+ public abstract void eat();
+ }
+ class Apple implements fruit{
+ public void eat(){
+ System.out.println("Apple");
+ }
+ }
+ class Orange implements fruit{
+ public void eat(){
+ System.out.println("Orange");
+ }
+ }
+ //构造工厂类
+ //也就是说以后如果我们在添加其他的实例的时候只需要修改工厂类就行了
+ class Factory{
+ public static fruit getInstance(String fruitName){
+ fruit f=null;
+ if("Apple".equals(fruitName)){
+ f=new Apple();
+ }
+ if("Orange".equals(fruitName)){
+ f=new Orange();
+ }
+ return f;
+ }
+ }
+ class hello{
+ public static void main(String[] a){
+ fruit f=Factory.getInstance("Orange");
+ f.eat();
+ }
+ }
+
+上面写法的缺点是当我们再添加一个子类的时候,就需要修改工厂类了。如果我们添加太多的子类的时候,改动就会很多。下面用反射机制实现工厂模式:
+
+ interface fruit{
+ public abstract void eat();
+ }
+ class Apple implements fruit{
+ public void eat(){
+ System.out.println("Apple");
+ }
+ }
+ class Orange implements fruit{
+ public void eat(){
+ System.out.println("Orange");
+ }
+ }
+ class Factory{
+ public static fruit getInstance(String ClassName){
+ fruit f=null;
+ try{
+ f=(fruit)Class.forName(ClassName).newInstance();
+ }catch (Exception e) {
+ e.printStackTrace();
+ }
+ return f;
+ }
+ }
+ class hello{
+ public static void main(String[] a){
+ fruit f=Factory.getInstance("Reflect.Apple");
+ if(f!=null){
+ f.eat();
+ }
+ }
+ }
+
+
+现在就算我们添加任意多个子类的时候,工厂类都不需要修改。使用反射机制实现的工厂模式可以通过反射取得接口的实例,但是需要传入完整的包和类名。而且用户也无法知道一个接口有多少个可以使用的子类,所以我们通过属性文件的形式配置所需要的子类。
+
+下面编写使用反射机制并结合属性文件的工厂模式(即IoC)。首先创建一个fruit.properties的资源文件:
+
+ apple=Reflect.Apple
+ orange=Reflect.Orange
+
+然后编写主类代码:
+
+ interface fruit{
+ public abstract void eat();
+ }
+ class Apple implements fruit{
+ public void eat(){
+ System.out.println("Apple");
+ }
+ }
+ class Orange implements fruit{
+ public void eat(){
+ System.out.println("Orange");
+ }
+ }
+ //操作属性文件类
+ class init{
+ public static Properties getPro() throws FileNotFoundException, IOException{
+ Properties pro=new Properties();
+ File f=new File("fruit.properties");
+ if(f.exists()){
+ pro.load(new FileInputStream(f));
+ }else{
+ pro.setProperty("apple", "Reflect.Apple");
+ pro.setProperty("orange", "Reflect.Orange");
+ pro.store(new FileOutputStream(f), "FRUIT CLASS");
+ }
+ return pro;
+ }
+ }
+ class Factory{
+ public static fruit getInstance(String ClassName){
+ fruit f=null;
+ try{
+ f=(fruit)Class.forName(ClassName).newInstance();
+ }catch (Exception e) {
+ e.printStackTrace();
+ }
+ return f;
+ }
+ }
+ class hello{
+ public static void main(String[] a) throws FileNotFoundException, IOException{
+ Properties pro=init.getPro();
+ fruit f=Factory.getInstance(pro.getProperty("apple"));
+ if(f!=null){
+ f.eat();
+ }
+ }
+ }
+
+运行结果:Apple
+
+### IOC容器的技术剖析
+
+IOC中最基本的技术就是“反射(Reflection)”编程,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象,这种编程方式可以让对象在生成时才被决定到底是哪一种对象。只要是在Spring中生产的对象都要在配置文件中给出定义,目的就是提高灵活性和可维护性。
+
+目前C#、Java和PHP5等语言均支持反射,其中PHP5的技术书籍中,有时候也被翻译成“映射”。有关反射的概念和用法,大家应该都很清楚。反射的应用是很广泛的,很多的成熟的框架,比如像Java中的Hibernate、Spring框架,.Net中NHibernate、Spring.NET框架都是把”反射“作为最基本的技术手段。
+
+反射技术其实很早就出现了,但一直被忽略,没有被进一步的利用。当时的反射编程方式相对于正常的对象生成方式要慢至少得10倍。现在的反射技术经过改良优化,已经非常成熟,反射方式生成对象和通常对象生成方式,速度已经相差不大了,大约为1-2倍的差距。
+
+我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言提供的反射机制,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。
+
+
+
+### 使用IOC框架应该注意什么
+
+使用IOC框架产品能够给我们的开发过程带来很大的好处,但是也要充分认识引入IOC框架的缺点,做到心中有数,杜绝滥用框架。
+
+1)软件系统中由于引入了第三方IOC容器,生成对象的步骤变得有些复杂,本来是两者之间的事情,又凭空多出一道手续,所以,我们在刚开始使用IOC框架的时候,会感觉系统变得不太直观。所以,引入了一个全新的框架,就会增加团队成员学习和认识的培训成本,并且在以后的运行维护中,还得让新加入者具备同样的知识体系。
+
+2)由于IOC容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。
+
+ 3)具体到IOC框架产品(比如Spring)来讲,需要进行大量的配置工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
+
+4)IOC框架产品本身的成熟度需要进行评估,如果引入一个不成熟的IOC框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。
+
+我们大体可以得出这样的结论:一些工作量不大的项目或者产品,不太适合使用IOC框架产品。另外,如果团队成员的知识能力欠缺,对于IOC框架产品缺乏深入的理解,也不要贸然引入。最后,特别强调运行效率的项目或者产品,也不太适合引入IOC框架产品,像WEB2.0网站就是这种情况。
diff --git a/docs/basics/java-basic/jms.md b/docs/basics/java-basic/jms.md
new file mode 100644
index 00000000..e69de29b
diff --git a/docs/basics/java-basic/junit.md b/docs/basics/java-basic/junit.md
new file mode 100644
index 00000000..160e327a
--- /dev/null
+++ b/docs/basics/java-basic/junit.md
@@ -0,0 +1,18 @@
+JUnit是一个Java语言的单元测试框架。它由肯特·贝克和埃里希·伽玛(Erich Gamma)建立,逐渐成为源于Kent Beck的sUnit的xUnit家族中为最成功的一个。 JUnit有它自己的JUnit扩展生态圈。
+
+JUnit 促进了“先测试后编码”的理念,强调建立测试数据的一段代码,可以先测试,然后再应用。这个方法就好比“测试一点,编码一点,测试一点,编码一点……”,增加了程序员的产量和程序的稳定性,可以减少程序员的压力和花费在排错上的时间。
+
+特点:
+
+* JUnit 是一个开放的资源框架,用于编写和运行测试。
+* 提供注释来识别测试方法。
+* 提供断言来测试预期结果。
+* 提供测试运行来运行测试。
+* JUnit 测试允许你编写代码更快,并能提高质量。
+* JUnit 优雅简洁。没那么复杂,花费时间较少。
+* JUnit 测试可以自动运行并且检查自身结果并提供即时反馈。所以也没有必要人工梳理测试结果的报告。
+* JUnit 测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件。
+* JUnit 在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色。 JUnit知识经常 和测试驱动开发的讨论融合在一起。可以参考Kent Beck的 《Test-Driven Development: By Example》一书(有中文版和影印版)。
+
+
+推荐一份JUnit的教程,可以帮助你快速的学习使用它:https://wiki.jikexueyuan.com/project/junit/
\ No newline at end of file
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 ComparableString 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
+![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