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

简介: 【JDK源码】String源码学习笔记

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

首先思考几个问题:

  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&trade; 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方法,如果常量池中存在相同的值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。

目录
相关文章
|
3月前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
70 0
|
2月前
|
存储 JSON NoSQL
redis基本数据结构(String,Hash,Set,List,SortedSet)【学习笔记】
这篇文章是关于Redis基本数据结构的学习笔记,包括了String、Hash、Set、List和SortedSet的介绍和常用命令。文章解释了每种数据结构的特点和使用场景,并通过命令示例演示了如何在Redis中操作这些数据结构。此外,还提供了一些练习示例,帮助读者更好地理解和应用这些数据结构。
redis基本数据结构(String,Hash,Set,List,SortedSet)【学习笔记】
|
3月前
|
算法 安全 Java
深入JDK源码:揭开ConcurrentHashMap底层结构的神秘面纱
【8月更文挑战第24天】`ConcurrentHashMap`是Java并发编程中不可或缺的线程安全哈希表实现。它通过精巧的锁机制和无锁算法显著提升了并发性能。本文首先介绍了早期版本中使用的“段”结构,每个段是一个带有独立锁的小型哈希表,能够减少线程间竞争并支持动态扩容以应对高并发场景。随后探讨了JDK 8的重大改进:取消段的概念,采用更细粒度的锁控制,并引入`Node`等内部类以及CAS操作,有效解决了哈希冲突并实现了高性能的并发访问。这些设计使得`ConcurrentHashMap`成为构建高效多线程应用的强大工具。
54 2
|
3月前
|
存储 编译器 C语言
C++ --> string类模拟实现(附源码)
C++ --> string类模拟实现(附源码)
68 4
|
3月前
|
存储 C++
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
42 0
|
3月前
|
存储 Java
构造String问题之在JDK 9及更高版本中,直接访问String对象的coder和value属性,如何实现
构造String问题之在JDK 9及更高版本中,直接访问String对象的coder和value属性,如何实现
|
5月前
|
编译器 C++
【C++】学习笔记——string_5
【C++】学习笔记——string_5
22 0
|
5月前
|
编译器 C语言 C++
【C++】学习笔记——string_4
【C++】学习笔记——string_4
27 0
|
5月前
|
C语言 C++
【C++】学习笔记——string_3
【C++】学习笔记——string_3
27 0
|
5月前
|
存储 编译器 C++
【C++】学习笔记——string_2
【C++】学习笔记——string_2
29 0