JDK源码-基础类-String

基础类,包括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、那几个成员变量展开的

额外探索

  1. jvm里有字符串常量池、运行时常量池,二者的区别是什么?

    参考文章,对jvm的内存区域进行了很好的讲解。

    总结来说:

    1. jvm内存分为 堆、方法区、虚拟机栈、本地方法栈、程序计数器,这个其实都是jvm规范,不是真正实现。
    2. 只是堆、栈、程序计数器没有特殊的地方,规范和实现相似。
    3. 栈还有一点特殊,HotSpot把虚拟机栈、本地方法栈实现在了同一个内存区域。
    4. 方法区就比较特殊了,其含义是存储类的元信息、方法字节码、运行时常量的地方。其实字符串常量也是一种常量,只是程序里使用的非常多,且内存占用比较大,所以单独进行了拆分优化而已。
    5. 方法区在jdk7以前是实现在永久代的,jdk8之后就实现在了元空间,使用本地内存避免oom。
    6. 然后,方法区的一些内容也随着迭代产生了一些位置变化,类的静态变量和字符串常量池从jdk7开始都挪到了堆中
  2. 字符串常量池存储的是什么内容?

    参考知乎问答,RednaxelaFX对字符串常量池进行了很多的回答。

    这两篇 文章1文章2里的示例可以结合做一下理解

    首先,我们需要确定一个概念,字符串常量池到底是什么。我认为字符串常量池应该是StringTable/String pool,它就是一个map,key可能是字面量或者哈希值,value是字符串对象的引用。

    那么字符串存在哪里了呢?对于intern的字符串对象,JDK1.6及以前是存在永久代的,JDK1.7及以后存在了堆上。

    字符串常量池,也是一样的,jdk1.7前后从永久代挪到了堆上,在jdk8之后更是挪到了元空间。

  3. 如果新创建一个String对象,如何做到复用的?如何在字符串常量池中查找到已经有这样一个对象存在的?

    首先要明确的一点是,使用字面量比如String s = "aa"和常量表达式比如 String s = "a"+"a"这样进行赋值的,会直接从常量池中查找对象并赋值给s。

    如果要使用String s = new String("a")这养的表达式,说明意图上就是要新建对象,绝不会复用常量池中的对象。

  4. 这两篇 文章1文章2里的示例,纠误

    这两篇文章里的运行结果都是对的,但是那几个图画的真是稀碎。我重新说明一下代码的执行过程。

    java 复制代码
    public 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行代码的执行时机。未做确认。

相关推荐
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于ssm的空中停车场管理系统为例,包含答辩的问题和答案
java
不愿是过客3 小时前
java实战干货——长方法深递归
java
u0109272713 小时前
C++中的策略模式变体
开发语言·c++·算法
雨季6664 小时前
构建 OpenHarmony 简易文字行数统计器:用字符串分割实现纯文本结构感知
开发语言·前端·javascript·flutter·ui·dart
雨季6664 小时前
Flutter 三端应用实战:OpenHarmony 简易倒序文本查看器开发指南
开发语言·javascript·flutter·ui
小北方城市网4 小时前
Redis 分布式锁高可用实现:从原理到生产级落地
java·前端·javascript·spring boot·redis·分布式·wpf
进击的小头4 小时前
行为型模式:策略模式的C语言实战指南
c语言·开发语言·策略模式
天马37984 小时前
Canvas 倾斜矩形绘制波浪效果
开发语言·前端·javascript
六义义4 小时前
java基础十二
java·数据结构·算法