8000 增加部分知识点 · coderli7/toBeTopJavaer@deed787 · GitHub
[go: up one dir, main page]

Skip to content

Commit deed787

Browse files
author
hollis.zhl
committed
增加部分知识点
1 parent e0014c9 commit deed787

File tree

4 files changed

+237
-6
lines changed

4 files changed

+237
-6
lines changed

docs/_sidebar.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777

7878
* 字符串池
7979

80-
* 常量池(运行时常量池、Class常量池)
80+
* 常量池(运行时常量池、[Class常量池](/basics/java-basic/class-contant-pool.md)
8181

8282
* intern
8383

@@ -129,10 +129,10 @@
129129

130130
* [Enumeration和Iterator区别](/basics/java-basic/Enumeration-vs-Iterator.md)
131131

132-
* 如何在遍历的同时删除ArrayList中的元素
133-
134132
* [fail-fast 和 fail-safe](/basics/java-basic/fail-fast-vs-fail-safe.md)
135133

134+
* [如何在遍历的同时删除ArrayList中的元素](/basics/java-basic/delete-while-iterator.md)
135+
136136
* [CopyOnWriteArrayList](/basics/java-basic/CopyOnWriteArrayList.md)
137137

138138
* [ConcurrentSkipListMap](/basics/java-basic/ConcurrentSkipListMap.md)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
在Java中,常量池的概念想必很多人都听说过。这也是面试中比较常考的题目之一。在Java有关的面试题中,一般习惯通过String的有关问题来考察面试者对于常量池的知识的理解,几道简单的String面试题难倒了无数的开发者。所以说,常量池是Java体系中一个非常重要的概念。
2+
3+
谈到常量池,在Java体系中,共用三种常量池。分别是**字符串常量池****Class常量池****运行时常量池**
4+
5+
本文先来介绍一下到底什么是Class常量池。
6+
7+
### 什么是Class文件
8+
9+
[Java代码的编译与反编译那些事儿][1]中我们介绍过Java的编译和反编译的概念。我们知道,计算机只认识0和1,所以程序员写的代码都需要经过编译成0和1构成的二进制格式才能够让计算机运行。
10+
11+
我们在《[深入分析Java的编译原理][2]》中提到过,为了让Java语言具有良好的跨平台能力,Java独具匠心的提供了一种可以在所有平台上都能使用的一种中间代码——字节码(ByteCode)。
12+
13+
有了字节码,无论是哪种平台(如Windows、Linux等),只要安装了虚拟机,都可以直接运行字节码。
14+
15+
同样,有了字节码,也解除了Java虚拟机和Java语言之间的耦合。这话可能很多人不理解,Java虚拟机不就是运行Java语言的么?这种解耦指的是什么?
16+
17+
其实,目前Java虚拟机已经可以支持很多除Java语言以外的语言了,如Groovy、JRuby、Jython、Scala等。之所以可以支持,就是因为这些语言也可以被编译成字节码。而虚拟机并不关心字节码是有哪种语言编译而来的。
18+
19+
Java语言中负责编译出字节码的编译器是一个命令是`javac`
20+
21+
> javac是收录于JDK中的Java语言编译器。该工具可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。
22+
23+
如,我们有以下简单的`HelloWorld.java`代码:
24+
25+
public class HelloWorld {
26+
public static void main(String[] args) {
27+
String s = "Hollis";
28+
}
29+
}
30+
31+
32+
通过javac命令生成class文件:
33+
34+
javac HelloWorld.java
35+
36+
37+
生成`HelloWorld.class`文件:
38+
39+
![][3]
40+
41+
> 如何使用16进制打开class文件:使用 `vim test.class` ,然后在交互模式下,输入`:%!xxd` 即可。
42+
43+
可以看到,上面的文件就是Class文件,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
44+
45+
要想能够读懂上面的字节码,需要了解Class类文件的结构,由于这不是本文的重点,这里就不展开说明了。
46+
47+
> 读者可以看到,`HelloWorld.class`文件中的前八个字母是`cafe babe`,这就是Class文件的魔数([Java中的”魔数”][4]
48+
49+
我们需要知道的是,在Class文件的4个字节的魔数后面的分别是4个字节的Class文件的版本号(第5、6个字节是次版本号,第7、8个字节是主版本号,我生成的Class文 6D40 件的版本号是52,这时Java 8对应的版本。也就是说,这个版本的字节码,在JDK 1.8以下的版本中无法运行)在版本号后面的,就是Class常量池入口了。
50+
51+
### Class常量池
52+
53+
Class常量池可以理解为是Class文件中的资源仓库。 Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
54+
55+
由于不同的Class文件中包含的常量的个数是不固定的,所以在Class文件的常量池入口处会设置两个字节的常量池容量计数器,记录了常量池中常量的个数。
56+
57+
![-w697][5]
58+
59+
当然,还有一种比较简单的查看Class文件中常量池的方法,那就是通过`javap`命令。对于以上的`HelloWorld.class`,可以通过
60+
61+
javap -v HelloWorld.class
62+
63+
64+
查看常量池内容如下:
65+
66+
![][6]
67+
68+
> 从上图中可以看到,反 9E88 编译后的class文件常量池中共有16个常量。而Class文件中常量计数器的数值是0011,将该16进制数字转换成10进制的结果是17。
69+
>
70+
> 原因是与Java的语言习惯不同,常量池计数器是从0开始而不是从1开始的,常量池的个数是10进制的17,这就代表了其中有16个常量,索引值范围为1-16。
71+
72+
### 常量池中有什么
73+
74+
介绍完了什么是Class常量池以及如何查看常量池,那么接下来我们就要深入分析一下,Class常量池中都有哪些内容。
75+
76+
常量池中主要存放两大类常量:字面量(literal)和符号引用(symbolic references)。
77+
78+
### 字面量
79+
80+
前面说过,运行时常量池中主要保存的是字面量和符号引用,那么到底什么字面量?
81+
82+
> 在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。
83+
84+
以上是关于计算机科学中关于字面量的解释,并不是很容易理解。说简单点,字面量就是指由字母、数字等构成的字符串或者数值。
85+
86+
字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=123这里的a为左值,123为右值。在这个例子中123就是字面量。
87+
88+
int a = 123;
89+
String s = "hollis";
90+
91+
92+
上面的代码事例中,123和hollis都是字面量。
93+
94+
本文开头的HelloWorld代码中,Hollis就是一个字面量。
95+
96+
### 符号引用
97+
98+
常量池中,除了字面量以外,还有符号引用,那么到底什么是符号引用呢。
99+
100+
符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量: * 类和接口的全限定名 * 字段的名称和描述符 * 方法的名称和描述符
101+
102+
这也就可以印证前面的常量池中还包含一些`com/hollis/HelloWorld``main``([Ljava/lang/String;)V`等常量的原因了。
103+
104+
### Class常量池有什么用
105+
106+
前面介绍了这么多,关于Class常量池是什么,怎么查看Class常量池以及Class常量池中保存了哪些东西。有一个关键的问题没有讲,那就是Class常量池到底有什么用。
107+
108+
首先,可以明确的是,Class常量池是Class文件中的资源仓库,其中保存了各种常量。而这些常量都是开发者定义出来,需要在程序的运行期使用的。
109+
110+
在《深入理解Java虚拟》中有这样的表述:
111+
112+
Java代码在进行`Javac`编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容,在虚拟机类加载过程时再进行详细讲解。
113+
114+
前面这段话,看起来很绕,不是很容易理解。其实他的意思就是: Class是用来保存常量的一个媒介场所,并且是一个中间场所。在JVM真的运行时,需要把常量池中的常量加载到内存中。
115+
116+
至于到底哪个阶段会做这件事情,以及Class常量池中的常量会以何种方式被加载到具体什么地方,会在本系列文章的后续内容中继续阐述。欢迎关注我的博客(http://www.hollischuang.com) 和公众号(Hollis),即可第一时间获得最新内容。
117+
118+
另外,关于常量池中常量的存储形式,以及数据类型的表示方法本文中并未涉及,并不是说这部分知识点不重要,只是Class字节码的分析本就枯燥,作者不想在一篇文章中给读者灌输太多的理论上的内容。感兴趣的读者可以自行Google学习,如果真的有必要,我也可以单独写一篇文章再深入介绍。
119+
120+
### 参考资料
121+
122+
《深入理解java虚拟机》 [《Java虚拟机原理图解》 1.2.2、Class文件中的常量池详解(上)][7]
123+
124+
[1]: http://www.hollischuang.com/archives/58
125+
[2]: http://www.hollischuang.com/archives/2322
126+
[3]: http://www.hollischuang.com/wp-content/uploads/2018/10/15401179593014.jpg
127+
[4]: http://www.hollischuang.com/archives/491
128+
[5]: http://www.hollischuang.com/wp-content/uploads/2018/10/15401192359009.jpg
129+
[6]: http://www.hollischuang.com/wp-content/uploads/2018/10/15401195127619.jpg
130+
[7]: https://blog.csdn.net/luanlouis/article/details/39960815
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
**1、直接使用普通for循环进行操作**
2+
3+
我们说不能在foreach中进行,但是使用普通的for循环还是可以的,因为普通for循环并没有用到Iterator的遍历,所以压根就没有进行fail-fast的检验。
4+
5+
List<String> userNames = new ArrayList<String>() {{
6+
add("Hollis");
7+
add("hollis");
8+
add("HollisChuang");
9+
add("H");
10+
}};
11+
12+
for (int i = 0; i < 1; i++) {
13+
if (userNames.get(i).equals("Hollis")) {
14+
userNames.remove(i);
15+
}
16+
}
17+
System.out.println(userNames);
18+
19+
20+
这种方案其实存在一个问题,那就是remove操作会改变List中元素的下标,可能存在漏删的情况。 **2、直接使用Iterator进行操作**
21+
22+
除了直接使用普通for循环以外,我们还可以直接使用Iterator提供的remove方法。
23+
24+
List<String> userNames = new ArrayList<String>() {{
25+
add("Hollis");
26+
add("hollis");
27+
add("HollisChuang");
28+
add("H");
29+
}};
30+
31+
Iterator iterator = userNames.iterator();
32+
33+
while (iterator.hasNext()) {
34+
if (iterator.next().equals("Hollis")) {
35+
iterator.remove();
36+
}
37+
}
38+
System.out.println(userNames);
39+
40+
41+
如果直接使用Iterator提供的remove方法,那么就可以修改到expectedModCount的值。那么就不会再抛出异常了。
42+
43+
44+
**3、使用Java 8中提供的filter过滤**
45+
46+
Java 8中可以把集合转换成流,对于流有一种filter操作, 可以对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。
47+
48+
List<String> userNames = new ArrayList<String>() {{
49+
add("Hollis");
50+
add("hollis");
51+
add("HollisChuang");
52+
add("H");
53+
}};
54+
55+
userNames = userNames.stream().filter(userName -> !userName.equals("Hollis")).collect(Collectors.toList());
56+
System.out.println(userNames);
57+
58+
59+
**4、使用增强for循环其实也可以**
60+
61+
如果,我们非常确定在一个集合中,某个即将删除的元素只包含一个的话, 比如对Set进行操作,那么其实也是可以使用增强for循环的,只要在删除之后,立刻结束循环体,不要再继续进行遍历就可以了,也就是说不让代码执行到下一次的next方法。
62+
63+
List<String> userNames = new ArrayList<String>() {{
64+
add("Hollis");
65+
add("hollis");
66+
add("HollisChuang");
67+
add("H");
68+
}};
69+
70+
for (String userName : userNames) {
71+
if (userName.equals("Hollis")) {
72+
userNames.remove(userName);
73+
break;
74+
}
75+
}
76+
System.out.println(userNames);
77+
78+
79+
**5、直接使用fail-safe的集合类**
80+
81+
在Java中,除了一些普通的集合类以外,还有一些采用了fail-safe机制的集合类。这样的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
82+
83+
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。
84+
85+
ConcurrentLinkedDeque<String> userNames = new ConcurrentLinkedDeque<String>() {{
86+
add("Hollis");
87+
add("hollis");
88+
add("HollisChuang");
89+
add("H");
90+
}};
91+
92+
for (String userName : userNames) {
93+
if (userName.equals("Hollis")) {
94+
userNames.remove();
95+
}
96+
}
97+
98+
99+
基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
100+
101+
java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

docs/menu.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
134134

135135
* 字符串池
136136

137-
* 常量池(运行时常量池、Class常量池)
137+
* 常量池(运行时常量池、[Class常量池](/basics/java-basic/class-contant-pool.md)
138138

139139
* intern
140140

@@ -186,10 +186,10 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)
186186

187187
* [Enumeration和Iterator区别](/basics/java-basic/Enumeration-vs-Iterator.md)
188188

189-
* 如何在遍历的同时删除ArrayList中的元素
190-
191189
* [fail-fast 和 fail-safe](/basics/java-basic/fail-fast-vs-fail-safe.md)
192190

191+
* [如何在遍历的同时删除ArrayList中的元素](/basics/java-basic/delete-while-iterator.md)
192+
193193
* [CopyOnWriteArrayList](/basics/java-basic/CopyOnWriteArrayList.md)
194194

195195
* [ConcurrentSkipListMap](/basics/java-basic/ConcurrentSkipListMap.md)

0 commit comments

Comments
 (0)
0