基础类,包括Object类、一些包装类、Class类
类的阅读从整体到细节,细节部分关注三个大问题,超出即停止;小问题可以作为阅读过程中的思考过程,不进行限制
String类
整体
String是字符串类,提供了存储表示、转换、查找的方法
接口
CharSequence接口
CharSequence代表字符序列,String只是其中一种实现,StringBuffer、CharBuffer等都是其实现类。
突然有种系统化的感觉,对于编程语言来说,应该是先考虑到有字符序列,String只是其中一种最方便和经常使用的结构,并提供了大量便捷的方法,本质上还是对字符序列的处理(迁移理解的话,c就是直接处理的char数组,c++和java都提供了更高层次的封装)
Constable, ConstantDesc两个接口
Constable是表示当前类"可常量化",可以用标识符识别这个常量;ConstantDesc就是标识符,同时它提供了基于标识符转回对象的操作。有点类似"对象"和"常量"之间的互转,这块没完全理解,暂不深入。
成员变量
byte[] value
存储是用的byte[] value,这个很有意思。之前以为是char数组,其实是byte数组。这是出于节省空间的考虑进行的优化。
char是2字节(16位),byte是1字节(8位)。String提供了两种编码,LATIN1(ISO-8859-1)和UTF16,前者最小单位是1字节,后者最小单位是2字节。如果每个字符的编码都比较小,可以用LATIN1编码。用UTF16编码的时候,需要把原来的char转成两个byte进行存储。这样最终统一使用byte数组来存储原始信息。
另外UTF16还涉及到了大端对齐和小端对齐。因为要用两个字节代表一个字符,那么两个字节的存储顺序就需要提前约定好,是高位在前还是低位在前,这样才能把两个byte还原成char。
为什么不使用utf8呢?因为utf8是一个变长编码,对于字符串来说有很多的定位查找方法,变长编码需要遍历整个数组,效率太低了。UTF8适合在存储和传输的时候减少信息大小,不适合在内存中的高效率处理。
byte coder
用来标识使用了什么编码。
因为只有两种编码,用byte只需要一个字节,比使用int好。
int hash和 boolean hashIsZero
hash用来缓存字符串的哈希值,在一次计算之后直接记录下来,避免多次重复计算。
因为hash默认是0,所以用了另一个变量来区分是默认值还是计算后的哈希值确实是0。之前考虑过能不能不使用两个字段就能解决问题,倒是可以做到,但是有前提条件。比如hash不可能为负数的情况下,可以将默认值赋为-1;比如哈希值可能只需要31位,那么可以用最高位来做到hashIsZero的作用。如果不能确定有这些前提条件,最好是多加一个字段,不必钻牛角尖。
方法介绍
构造方法,主要是围绕value、那几个成员变量展开的
额外探索
-
jvm里有字符串常量池、运行时常量池,二者的区别是什么?
参考文章,对jvm的内存区域进行了很好的讲解。
总结来说:
- jvm内存分为 堆、方法区、虚拟机栈、本地方法栈、程序计数器,这个其实都是jvm规范,不是真正实现。
- 只是堆、栈、程序计数器没有特殊的地方,规范和实现相似。
- 栈还有一点特殊,HotSpot把虚拟机栈、本地方法栈实现在了同一个内存区域。
- 方法区就比较特殊了,其含义是存储类的元信息、方法字节码、运行时常量的地方。其实字符串常量也是一种常量,只是程序里使用的非常多,且内存占用比较大,所以单独进行了拆分优化而已。
- 方法区在jdk7以前是实现在永久代的,jdk8之后就实现在了元空间,使用本地内存避免oom。
- 然后,方法区的一些内容也随着迭代产生了一些位置变化,类的静态变量和字符串常量池从jdk7开始都挪到了堆中
-
字符串常量池存储的是什么内容?
参考知乎问答,RednaxelaFX对字符串常量池进行了很多的回答。
首先,我们需要确定一个概念,字符串常量池到底是什么。我认为字符串常量池应该是StringTable/String pool,它就是一个map,key可能是字面量或者哈希值,value是字符串对象的引用。
那么字符串存在哪里了呢?对于intern的字符串对象,JDK1.6及以前是存在永久代的,JDK1.7及以后存在了堆上。
字符串常量池,也是一样的,jdk1.7前后从永久代挪到了堆上,在jdk8之后更是挪到了元空间。
-
如果新创建一个String对象,如何做到复用的?如何在字符串常量池中查找到已经有这样一个对象存在的?
首先要明确的一点是,使用字面量比如String s = "aa"和常量表达式比如 String s = "a"+"a"这样进行赋值的,会直接从常量池中查找对象并赋值给s。
如果要使用String s = new String("a")这养的表达式,说明意图上就是要新建对象,绝不会复用常量池中的对象。
-
这两篇文章里的运行结果都是对的,但是那几个图画的真是稀碎。我重新说明一下代码的执行过程。
javapublic static void main(String[] args) { String s = new String("1"); #1 s.intern(); #2 String s2 = "1"; #3 System.out.println(s == s2); #4 String s3 = new String("1") + new String("1"); #5 s3.intern(); #6 String s4 = "11"; #7 System.out.println(s3 == s4); #8 }#之后的是行号
第1行执行时,字面量"1"会生成一个字符串对象,并记录到常量池中;然后调用构造方法new String("1")生成新对象赋值给s。这样会生成两个对象。
第2行执行时,因为常量池中已经有了"1"这个对象,所以不会新生成对象。
第3行执行时,从常量池中查找到对象赋值给s2
第4行,一定为false,不管jdk版本是啥
第5行执行时,字面量"1"会在常量池中查找到对象,然后调用两次构造方法new String("1")生成两个新对象。然后会隐藏生成StringBuilder,调用append方法拼接,并调用toString方法生成新的对象赋值给s3(这一段逻辑可以通过javap -v className来反解析class文件的方式查看指令码得到验证 )。这里生成了4个对象。
第6行执行时,常量池中没有"11",所以需要往字符串常量池中增加记录。至于常量池中记录的对象,在jdk1.6之前是新生成了一个字符串对象放在了永久代,并在常量池中指向它;在jdk1.7之后是直接在常量池中指向了s3这个原来的对象。这里的差异造成了第8行结果的差异。
第7行执行时,常量池中已有"11",查出来赋值给s4
第8行,jdk1.6时返回false,jdk1.7以后返回true
所以,引文里的图片上,常量池里的对象完全没说明白是怎么来的,intern的画线也没说明是在干什么,一头雾水。
还有一个问题,其实第8行在jdk17的时候返回的也是false,这可能是因为字面量"11"放入常量池的时机早于第7行代码的执行时机。未做确认。