先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip204888 (备注大数据)
正文
1 2 3 4 5 | public AbstractStringBuilder append(String str) { if (str == null``) return appendNull(); //... } |
如果append
方法的参数字符串为null
,那么这里会调用其父类AbstractStringBuilder
的appendNull
方法:
1 2 3 4 5 6 7 8 9 10 11 | private AbstractStringBuilder appendNull() { int c = count; ensureCapacityInternal(c + 4``); final char``[] value = this``.value; value[c++] = 'n'``; value[c++] = 'u'``; value[c++] = 'l'``; value[c++] = 'l'``; count = c; return this``; } |
这里的value
就是底层用来存储字符的char
类型数组,到这里我们就可以明白了,其实StringBuilder
也对null
的字符串进行了特殊处理,在append
的过程中如果碰到是null
的字符串,那么就会以"null"
的形式被添加进字符数组,这也就导致了两个为空null
的字符串相加后会打印为"nullnull"
。
第2题,改变String的值
如何改变一个String字符串的值,这道题可能看上去有点太简单了,像下面这样直接赋值不就可以了吗?
1 2 | String s=``"Hydra"``; s=``"Trunks"``; |
恭喜你,成功掉进了坑里!在回答这道题之前,我们需要知道String是不可变的,打开String的源码在开头就可以看到:
1 | private final char value[]; |
可以看到,String的本质其实是一个char
类型的数组,然后我们再看两个关键字。先看final
,我们知道final
在修饰引用数据类型时,就像这里的数组时,能够保证指向该数组地址的引用不能修改,但是数组本身内的值可以被修改。
是不是有点晕,没关系,我们看一个例子:
1 2 3 | final char``[] one={``'a'``,``'b'``,``'c'``}; char``[] two={``'d'``,``'e'``,``'f'``}; one=two; |
如果你这样写,那么编译器是会报错提示Cannot assign a value to final variable 'one'
,说明被final
修饰的数组的引用地址是不可改变的。但是下面这段代码却能够正常的运行:
1 2 | final char``[] one={``'a'``,``'b'``,``'c'``}; one[``1``]=``'z'``; |
也就是说,即使被final
修饰,但是我直接操作数组里的元素还是可以的,所以这里还加了另一个关键字private
,防止从外部进行修改。此外,String类本身也被添加了final
关键字修饰,防止被继承后对属性进行修改。
到这里,我们就可以理解为什么String是不可变的了,那么在上面的代码进行二次赋值的过程中,发生了什么呢?答案很简单,前面的变量s
只是一个String对象的引用,这里的重新赋值时将变量s
指向了新的对象。
上面白话了一大顿,其实是我们可以通过比较hashCode
的方式来看一下引用指向的对象是否发生了改变,修改一下上面的代码,打印字符串的hashCode
:
1 2 3 4 5 6 | public static void main(String[] args) { String s=``"Hydra"``; System.out.println(s+``": "``+s.hashCode()); s=``"Trunks"``; System.out.println(s+``": "``+s.hashCode()); } |
查看结果,发生了改变,证明指向的对象发生了改变:
那么,回到上面的问题,如果我想要改变一个String的值,而又不想把它重新指向其他对象的话,应该怎么办呢?答案是利用反射修改char
数组的值:
1 2 3 4 5 6 7 8 9 | public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { String s=``"Hydra"``; System.out.println(s+``": "``+s.hashCode()); Field field = String.``class``.getDeclaredField(``"value"``); field.setAccessible(``true``); field.set(s,``new char``[]{``'T'``,``'r'``,``'u'``,``'n'``,``'k'``,``'s'``}); System.out.println(s+``": "``+s.hashCode()); } |
再对比一下hashCode
,修改后和之前一样,对象没有发生任何变化:
最后,再啰嗦说一点题外话,这里看的是jdk8
中String的源码,到这为止还是使用的char
类型数组来存储字符,但是在jdk9
中这个char
数组已经被替换成了byte
数组,能够使String对象占用的内存减少。
第3题,创建了几个对象?
相信不少小伙伴在面试中都遇到过这道经典面试题,下面这段代码中到底创建了几个对象?
1 | String s = new String(``"Hydra"``); |
其实真正想要回答好这个问题,要铺垫的知识点还真是不少。首先,我们需要了解3个关于常量池的概念,下面还是基于jdk8
版本进行说明:
- class文件常量池:在class文件中保存了一份常量池(
Constant Pool
),主要存储编译时确定的数据,包括代码中的字面量(literal
)和符号引用 - 运行时常量池:位于方法区中,全局共享,class文件常量池中的内容会在类加载后存放到方法区的运行时常量池中。除此之外,在运行期间可以将新的变量放入运行时常量池中,相对class文件常量池而言运行时常量池更具备动态性
- 字符串常量池:位于堆中,全局共享,这里可以先粗略的认为它存储的是String对象的直接引用,而不是直接存放的对象,具体的实例对象是在堆中存放
可以用一张图来描述它们各自所处的位置:
接下来,我们来细说一下字符串常量池的结构,其实在Hotspot JVM中,字符串常量池StringTable
的本质是一张HashTable
,那么当我们说将一个字符串放入字符串常量池的时候,实际上放进去的是什么呢?
以字面量的方式创建String对象为例,字符串常量池以及堆栈的结构如下图所示(忽略了jvm中的各种OopDesc
实例):
实际上字符串常量池HashTable
采用的是数组加**链表的结构,链表中的节点是一个个的HashTableEntry
,而HashTableEntry
中的value
则存储了堆上String对象的引用**。
那么,下一个问题来了,这个字符串对象的引用是什么时候被放到字符串常量池中的?具体可为两种情况:
- 使用字面量声明String对象时,也就是被双引号包围的字符串,在堆上创建对象,并驻留到字符串常量池中(注意这个用词)
- 调用
intern()
方法,当字符串常量池没有相等的字符串时,会保存该字符串的引用
注意!我们在上面用到了一个词驻留,这里对它进行一下规范。当我们说驻留一个字符串到字符串常量池时,指的是创建HashTableEntry
,再使它的value
指向堆上的String实例,并把HashTableEntry
放入字符串常量池,而不是直接把String对象放入字符串常量池中。简单来说,可以理解为将String对象的引用保存在字符串常量池中。
我们把intern()
方法放在后面细说,先主要看第一种情况,这里直接整理引用R大的结论:
在类加载阶段,JVM会在堆中创建对应这些class文件常量池中的字符串对象实例,并在字符串常量池中驻留其引用。
这一过程具体是在resolve阶段(个人理解就是resolution解析阶段)执行,但是并不是立即就创建对象并驻留了引用,因为在JVM规范里指明了resolve阶段可以是lazy的。CONSTANT_String会在第一次引用该项的ldc指令被第一次执行到的时候才会resolve。
就HotSpot VM的实现来说,加载类时字符串字面量会进入到运行时常量池,不会进入全局的字符串常量池,即在StringTable中并没有相应的引用,在堆中也没有对应的对象产生。
这里大家可以暂时先记住这个结论,在后面还会用到。
在弄清楚上面几个概念后,我们再回过头来,先看看用字面量声明String的方式,代码如下:
1 2 3 | public static void main(String[] args) { String s = "Hydra"``; } |
反编译生成的字节码文件:
1 2 3 4 5 6 7 8 | public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=``1``, locals=``2``, args_size=``1 0``: ldc #``2 // String Hydra 2``: astore_1 3``: return |
解释一下上面的字节码指令:
0: ldc
,查找后面索引为#2
对应的项,#2
表示常量在常量池中的位置。在这个过程中,会触发前面提到的lazy resolve,在resolve过程如果发现StringTable
已经有了内容匹配的String引用,则直接返回这个引用,反之如果StringTable
里没有内容匹配的String对象的引用,则会在堆里创建一个对应内容的String对象,然后在StringTable
驻留这个对象引用,并返回这个引用,之后再压入操作数栈中2: astore_1
,弹出栈顶元素,并将栈顶引用类型值保存到局部变量1中,也就是保存到变量s
中3: return
,执行void
函数返回
可以看到,在这种模式下,只有堆中创建了一个"Hydra"
对象,在字符串常量池中驻留了它的引用。并且,如果再给字符串s2
、s3
也用字面量的形式赋值为"Hydra"
,它们用的都是堆中的唯一这一个对象。
好了,再看一下以构造方法的形式创建字符串的方式:
1 2 3 | public static void main(String[] args) { String s = new String(``"Hydra"``); } |
同样反编译这段代码的字节码文件:
1 2 3 4 5 6 7 8 9 10 11 | public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=``3``, locals=``2``, args_size=``1 0``: new #``2 // class java/lang/String 3``: dup 4``: ldc #``3 // String Hydra 6``: invokespecial #``4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9``: astore_1 10``: return |
看一下和之前不同的字节码指令部分:
0: new
,在堆上创建一个String对象,并将它的引用压入操作数栈,注意这时的对象还只是一个空壳,并没有调用类的构造方法进行初始化3: dup
,复制栈顶元素,也就是复制了上面的对象引用,并将复制后的对象引用压入栈顶。这里之所以要进行复制,是因为之后要执行的构造方***从操作数栈弹出需要的参数和这个对象引用本身(这个引用起到的作用就是构造方法中的this
指针),如果不进行复制,在弹出后会无法得到初始化后的对象引用4: ldc
,在堆上创建字符串对象,驻留到字符串常量池,并将字符串的引用压入操作数栈6: invokespecial
,执行String的构造方法,这一步执行完成后得到一个完整对象
到这里,我们可以看到一共创建了两个String对象,并且两个都是在堆上创建的,且字面量方式创建的String对象的引用被驻留到了字符串常量池中。而栈里的s
只是一个变量,并不是实际意义上的对象,我们不把它包括在内。
其实想要验证这个结论也很简单,可以使用idea中强大的debug功能来直观的对比一下对象数量的变化,先看字面量创建String方式:
这个对象数量的计数器是在debug时,点击下方右侧Memory
的Load classes
弹出的。对比语句执行前后可以看到,只创建了一个String对象,以及一个char数组对象,也就是String对象中的value
。
再看看构造方法创建String的方式:
可以看到,创建了两个String对象,一个char数组对象,也说明了两个String中的value
指向了同一个char数组对象,符合我们上面从字节码指令角度解释的结果。
最后再看一下下面的这种情况,当字符串常量池已经驻留过某个字符串引用,再使用构造方法创建String时,创建了几个对象?
1 2 3 4 | public static void main(String[] args) { String s = "Hydra"``; String s2 = new String(``"Hydra"``); } |
答案是只创建一个对象,对于这种重复字面量的字符串,看一下反编译后的字节码指令:
1 2 3 4 5 6 7 8 9 10 | Code: stack=``3``, locals=``3``, args_size=``1 0``: ldc #``2 // String Hydra 2``: astore_1 3``: new #``3 // class java/lang/String 6``: dup 7``: ldc #``2 // String Hydra 9``: invokespecial #``4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 12``: astore_2 13``: return |
可以看到两次执行ldc
指令时后面索引相同,而ldc
判断是否需要创建新的String实例的依据是根据在第一次执行这条指令时,StringTable
是否已经保存了一个对应内容的String实例的引用。所以在第一次执行ldc
时会创建String实例,而在第二次ldc
就会直接返回而不需要再创建实例了。
第4题,烧脑的 intern
上面我们在研究字符串对象的引用如何驻留到字符串常量池中时,还留下了调用intern
方法的方式,下面我们来具体分析。
从字面上理解intern
这个单词,作为动词时它有禁闭、关押的意思,通过前面的介绍,与其说是将字符串关押到字符串常量池StringTable
中,可能将它理解为缓存它的引用会更加贴切。
String的intern()
是一个本地方法,可以强制将String驻留进入字符串常量池,可以分为两种情况:
- 如果字符串常量池中已经驻留了一个等于此String对象内容的字符串引用,则返回此字符串在常量池中的引用
- 否则,在常量池中创建一个引用指向这个String对象,然后返回常量池中的这个引用
好了,我们下面看一下这段代码,它的运行结果应该是什么?
1 2 3 4 5 6 7 | public static void main(String[] args) { String s1 = new String(``"Hydra"``); String s2 = s1.intern(); System.out.println(s1 == s2); System.out.println(s1 == "Hydra"``); System.out.println(s2 == "Hydra"``); } |
输出打印:
1 2 3 | false false true |
用一张图来描述它们的关系,就很容易明白了:
其实有了第三题的基础,了解这个结构已经很简单了:
- 在创建
s1
的时候,其实堆里已经创建了两个字符串对象StringObject1
和StringObject2
,并且在字符串常量池中驻留了StringObject2
- 当执行
s1.intern()
方法时,字符串常量池中已经存在内容等于"Hydra"
的字符串StringObject2
,直接返回这个引用并赋值给s2
s1
和s2
指向的是两个不同的String对象,因此返回 fasles2
指向的就是驻留在字符串常量池的StringObject2
,因此s2=="Hydra"
为 true,而s1
指向的不是常量池中的对象引用所以返回false
上面是常量池中已存在内容相等的字符串驻留的情况,下面再看看常量池中不存在的情况,看下面的例子:
1 2 3 4 5 6 | public static void main(String[] args) { String s1 = new String(``"Hy"``) + new String(``"dra"``); s1.intern(); String s2 = "Hydra"``; System.out.println(s1 == s2); } |
执行结果:
1 | true |
简单分析一下这个过程,第一步会在堆上创建"Hy"
和"dra"
的字符串对象,并驻留到字符串常量池中。
接下来,完成字符串的拼接操作,前面我们说过,实际上jvm会把拼接优化成StringBuilder
的append
方法,并最终调用toString
方法返回一个String对象。在完成字符串的拼接后,字符串常量池中并没有驻留一个内容等于"Hydra"
的字符串。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
串的拼接操作,前面我们说过,实际上jvm会把拼接优化成StringBuilder
的append
方法,并最终调用toString
方法返回一个String对象。在完成字符串的拼接后,字符串常量池中并没有驻留一个内容等于"Hydra"
的字符串。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
[外链图片转存中…(img-krVXqMda-1713693477473)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!