首页 > 编程语言 >【JDK源码】String源码学习笔记

【JDK源码】String源码学习笔记

时间:2024-01-02 10:36:54浏览次数:48  
标签:String JDK 对象 System s3 源码 字符串 常量

代码运行环境:JDK 8

首先思考几个问题:

  1. String对象在不同的JDK中是如何实现的?
  2. String对象的不可变性是什么样的?
  3. 下面这段代码的输出结果是什么?
String s1 = new String("aaa")+new String("");
s1.intern();
String s2 = "aaa";
System.out.println(s1==s2);

String s3 = new String("bbb")+new String("");
s3 = s3.intern();
String s4 = "bbb";
System.out.println(s3==s4);

String s5 = new String("hi") + new String("j");
s5.intern();
String s6 = "hij";
System.out.println(s5 == s6);

String对象的实现

Java6 及以前的版本

String对象是对 char 数组的封装,主要包括四个成员变量:char数组、偏移量offset、字符数量count、哈希值hash

使用 offset 和 count 两个属性可以定位 char 数组,获取字符串。这么做可以高效、快速地共享数组对象,同时节省内存空间。

缺点是可能导致内存泄露。

Java7 和 Java8 版本

去掉了 offset 和 count 两个变量,String对象占用的空间变少了。

String.substring方法不再共享char[],解决了可能导致的内存泄漏问题

Java9 版本

使用byte[]替换char[],新增了一个属性 coder,这是一个编码格式的标识。

在Java程序中,String占用的空间最大,然后大多数String只有 Latin-1 字符,Latin-1 字符只需要一个字节就够了。一个 char 字符占 2 个字节,就有很多空间要浪费。JDK1.9的 String 类为了节约内存空间,于是使用了1个字节的 byte 数组来存放字符串。

coder的作用是,在计算字符串长度或者使用indexOf()函数时,我们需要根据这个字段,判断如何计算字符串长度。coder属性默认有0和1两个值,0代表Latin-1(单字节编码),1代表UTF-16。如果String判断字符串只包含了Latin-1,则coder属性值为0,反之则为1。

String 对象的不可变性

先看看下面的这段代码:

String str = "Hello";
str = "World";

咦,str 它变了,说好的String对象不可变性呢?

这里涉及到两个含义,一个是对象本身,一个是对象引用。对象本身是内存中的一块内存地址。对象引用则是指向该内存地址的引用。在这两行代码里面,str 只是一个对象引用。

第一行代码执行的时候,创建了一个字符串对象 ”Hello“,然后 str 指向 ”Hello“ 对象的地址。第二行代码,创建了一个字符串对象 “World” 对象,str 重新指向了 “World”对象的地址。“Hello” 和 “World” 对象都没有改变,改变的只是对象引用 str 的值。

再来聊聊 String 类的代码实现

String 类被 final 关键字修饰了,所以这个类不能被继承,final类的方法默认都是 final 的(final方法不能被子类的方法覆盖,但可以被继承)。

String 类的属性char[]也被finalprivate修饰了。final成员变量表示常量,只能被赋值一次,赋值后值不再改变。这就意味着String对象一旦创建成功,不能再对它进行改变。这也是String对象的不可变性。

优点

  1. 保证String对象的安全性,防止了可能出现的恶意修改
  2. 保证hash属性值不会频繁变更,确保了唯一性,使得类似HashMap容器才能实现相应的key-value缓存功能。
  3. 可以实现字符串常量池。

创建字符串对象的方式

// 字符串常量
String str = "Hello World";
// 构造方法
String str = new String("Hello World");

使用第一种方式的时候,JVM会检查字符串常量池中是否存在该字符串,如果存在则直接返回该对象的引用,否则在会在常量池中创建这个字符串。这种方式可以减少同一个值得字符串对象的重复创建,节约内存。

使用第二种方式,首先在编译类文件的时候,“Hello World” 常量字符串会被放入常量结构中,在类加载的时候,“Hello World” 将在常量池中创建,然后在调用构造函数的时候,引用常量池中“Hello World”字符串,在堆内存中创建一个String对象,最后,str 变量引用String对象。

String对象的优化

1、构建超大字符串

使用+拼接字符串的时候,会被编译器优化成StringBuilder的方式,但是优化的时候,如果存在循环的情况,可能会创建多个StringBuilder实例,所以可以显示的使用StringBuilder拼接字符串。

在多线程编程中,可以使用StringBuffer。

2、字符串的分割

因为正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致CPU居高不下。

所以我们应该慎重使用Split()方法,我们可以用String.indexOf()方法代替Split()方法完成字符串的分割。如果实在无法满足需求,你就在使用Split()方法时,对回溯问题加以重视就可以了。

3、使用String.intern节约内存

官方对intern()方法的解释如下所示:

/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return  a string that has the same contents as this string, but is
*          guaranteed to be from a pool of unique strings.
*/
public native String intern();

当调用intern方法时,会去查看字符串常量池中是否有等于该对象的字符串的引用,如果有,就返回常量池中的字符串引用。

如果没有的话要分两种情况讨论:

  1. 在JDK1.6版本中,会复制堆中的字符串到常量池中,并返回该字符串引用,堆内存中原有的字符串由于没有引用指向它,将会通过垃圾回收器回收。
  2. 在JDK1.7版本以后,由于常量池已经合并到了堆中,所以不会再复制具体字符串了,只是会把首次遇到的字符串的引用添加到常量池中;

现在再看看开头的那三段代码,第一次比较的代码,如下所示:

// 创建两个对象,常量池一个,s1为字符串引用对象,两者指向对中的同一块"aaa"
String s1 = new String("aaa");
System.out.println("aaa:"+System.identityHashCode("aaa"));// 621009875
System.out.println("s1:"+System.identityHashCode(s1));// 1265094477

// 空串对象
String s2 = new String("");
// s1+s2以后,s3变成一个新的对象
String s3 = s1+s2;
System.out.println("s3:"+System.identityHashCode(s3));// 2125039532
// s1和s3指向同一块地址,所以""没在常量池中?
// 此处调用intern方法,因为常量池中包含"aaa"字符串,所以并没有实质性改变
System.out.println("s1 VS s3:"+(s1.intern()==s3.intern()));// true
// 调用intern方法前后,s3字符串并没有发生变化
System.out.println("s3:"+System.identityHashCode(s3));// 2125039532
// s4指向常量池的对象,所以s3和s4的内存地址不一致,所以最后返回false
String s4 = "aaa";
System.out.println("s4:"+System.identityHashCode(s4));// 621009875
System.out.println(s3==s4);// false

第二次比较的代码就容易理解了,正如上面提到的inter方法的作用,s3 = s3.intern();,返回值为字符串常量池中的字符串对象,所以s3的内存地址发生了变化,s3和s4相等。

最后看第三次比较的内容,这是一段很有意思的代码。

// 此处共创建了几个对象?
// 常量池"hi","j",以及hi和j的字符串实例对象,s5也是字符串实例对象“hij”,注意此时常量池中并没有"hij"字符串
String s5 = new String("hi") + new String("j");
// 此时s5对象的内存地址为621009875
System.out.println("s5:"+System.identityHashCode(s5));// 621009875
// 调用intern方法,这一步很重要,因为常量池中并没有"hij"字符串,所有s5对象引用复制到hashtable中,字符串常量池和s5指向同一块内存
s5.intern();
// 打印出s5的内存地址,确认s5并没有变化
System.out.println("s5:"+System.identityHashCode(s5));// 621009875
// 创建一个字符串常量,因为字符串常量池中已经存在,直接引用
String s6 = "hij";
// 打印s6的内存地址,内存地址和s5的一致,所以s5和s6相等
System.out.println("s6:"+System.identityHashCode(s6));// 621009875
System.out.println(s5 == s6); // true

如何利用intern方法节约内存呢?

假设要创建一些用户对象,这些用户在地址信息上有重合的,比如省份,城市等信息,这时候我们可以在每次赋值的时候使用String类的intern方法,如果常量池中存在相同的值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。

标签:String,JDK,对象,System,s3,源码,字符串,常量
From: https://blog.51cto.com/u_15812995/9063855

相关文章

  • 【JDK源码】Java中LinkedList的实现
    JDK版本:1.8.0_271基础介绍LinkedList底层数据结构是一个双向链表:链表的每个节点叫做Node,在Node中,prev属性表示前一个节点的位置,next属性表示后一个节点的位置first是双向链表的头节点,它的前一个节点是nulllast是双向链表的尾节点,它的后一个节点是null当链表中没有数据时,fi......
  • 【JDK源码】Java包装类的缓存是怎么回事
    JDK版本:1.8.0_271基础介绍缓存机制包装类是对Java中基本类型的封装,在JDK5中引入了包装类的缓存机制,有助于节省内存。实现方式是在类初始化的时,提前创建好会频繁使用的包装类对象,当需要使用某个类的包装类对象时,如果该对象包装的值在缓存的范围内,就返回缓存的对象,否则就创建新的......
  • 【JDK源码】ArrayList的代码实现
    JDK版本:1.8.0_271基础介绍ArrayList底层数据结构就是一个数组:index表示数组下标,从0开始计数,elementDatda表示数组本身DEFAULT_CAPACITY表示数组的初始化大小,默认是10size表示数组的大小,int类型,没有使用volatile修饰,非线程安全modCount统计当前数组被修改的版本次数,数......
  • JDK9中的String底层实现为什么用UTF-16而不用UTF-8呢?
    UTF-8是一种对空间利用效率最高的编码集,它是不定长的,使用1~4字节为每个字符编码。这种情况下,如果能用一个字节存放字符就不会使用两个字节,两个字节不够就用三个字节。这种编码集只适用于传输和存储,并不适合拿来做String的底层实现。String有随机访问的方法,比如charAt、subString等......
  • java基础知识点API之String详解--String基础看它就够了
    一:概述java中的String在java.lang包下,使用时可以直接使用不需要进行导包。字符串在日常使用中非常多,例如之前的变量定义。二:详细说明<1>JDK-帮助文档中对Strng类的介绍<2>字符串常量的创建,字符串常量在创建之后,它们的值不能被更改,但是可以被共享。publicstaticvoidmain(String[......
  • Java超高精度无线定位技术--UWB (超宽带)人员定位系统源码
    UWB室内定位技术是一种全新的、与传统通信技术有极大差异的通信新技术。它不需要使用传统通信体制中的载波,而是通过发送和接收具有纳秒或纳秒级以下的极窄脉冲来传输数据,从而具有GHz量级的带宽。UWB(超宽带)高精度定位系统是一种利用超宽带技术实现精确定位的解决方案。该系统使用高......
  • java中小微医疗机构云服务(云HIS)平台源码
    云HIS(Cloud-BasedHealthcareInformationSystem)重新定义了HIS,目标是为中小型医疗卫生机构提供优质经济的医疗卫生信息化产品及服务;是以健康档案为主线、以电子病历为核心、以云计算技术为基础的医疗卫生系统。云HIS作为基于云计算的B/S构架的HIS系统,为基层医疗机构(包括诊所、社区......
  • Linux内核bind系统调用源码分析
    一、环境说明内核版本:Linux3.10内核源码地址:https://elixir.bootlin.com/linux/v3.10/source(包含各个版本内核源码,且网页可全局搜索函数)二、应用层-bind()函数将socket套接字绑定指定的地址:/**sockfd:由socket函数返回的套接口描述符*sockaddr:一个指向特定于协议......
  • 【B/S架构】医院不良事件报告系统源码
    医院不良事件报告系统为医院内质量控制、患者安全关注、医疗安全不良事件方面的精细化的管理提供了平台,是等级医院评审的必备内容,评审要求医院直报系统要与卫生部“医疗安全(不良)事件报告系统”建立网络对接。不良事件报告系统源码包括护理相关事件、医疗相关事件、药件相关事件......
  • SaaS版Java基层健康卫生云HIS信息管理平台源码
    云his系统源码,系统采用主流成熟技术开发,B/S架构,软件结构简洁、代码规范易阅读,SaaS应用,全浏览器访问,前后端分离,多服务协同,服务可拆分,功能易扩展。多集团统一登录患者主索引建立、主数据管理,统一对外接口管理。1.系统管理及基础数据:科室、病区、人员及其岗位、权限管理,各种基础资料......