我想对于String这个类,没有谁对它陌生吧。可以说是无论在哪个项目中都是可以用到的。
那么反问一下你,你确定你对于String已经是真的了解了吗?你是否清楚String的内存分配?你是否清楚字节码文件中,它是如何的?你是否清楚创建String对象时,它牵扯到那几个知识点勒?一起来讨论吧。
”八小时内谋生活,八小时外谋发展“
共勉
封面地点
:湖南永州市蓝山县舜河村
作者
:用心笑*
注:
本文讨论的String 是Jdk8中的。
一、String基本特性
1.1、基础知识
- String 的创建方式
- String str1 = “你好丫”; 采取字面量的定义方式,字符串会存储在公共池中
- String str2 =new String("hello"); 采取new 对象的方式,会存储在堆中
- String 声明是final类型的,不可继承。
- String 实现了Serializable和Comparable接口:即字符串是支持序列化和比较大小的。
public final class String implements java.io.Serializable, Comparable<String>
- String在JDK 8 及之前,内部定义了``private final char[] value;
来存储字符串数据。但在jdk9 和11中已经改变为:
private final byte[] value;`来存储字符串数据。
我的电脑中暂时只有这几个版本,之后有空了会全部验证,大家也可以给出建议
1.2、大家想一想为什么会作出这样的改变勒?
The current implementation of the
String
class stores characters in achar
array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that mostString
objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internalchar
arrays of suchString
objects is going unused.
译为:
String类的当前实现将字符存储在字符数组中,每个字符使用两个字节(16位)。从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含拉丁字符1。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部字符数组中有一半的空间没有使用。 😚😯😲🙃😱
描述:
We propose to change the internal representation of the
String
class from a UTF-16char
array to abyte
array plus an encoding-flag field. The newString
class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.String-related classes such as
AbstractStringBuilder
,StringBuilder
, andStringBuffer
will be updated to use the same representation, as will the HotSpot VM's intrinsic string operations.This is purely an implementation change, with no changes to existing public interfaces. There are no plans to add any new public APIs or other interfaces.
The prototyping work done to date confirms the expected reduction in memory footprint, substantial reductions of GC activity, and minor performance regressions in some corner cases.
我们建议将字符串类的内部表示从UTF-16 Char数组更改为字节数组以及编码标志字段。 基于字符串的内容将新的字符串类存储为ISO-8859-1 / LATIN-1(每个字符)或UTF-16(每个字符)(每种字符为两个字节)的字符。 编码标志将指示使用了哪个编码。🐱🏍
将更新字符串相关类,如AbstractStringBuilder,StringBuilder和StringBuffer以使用相同的表示,HotSpot VM的内部字符串操作也是如此。😼
这纯粹是一个实现变化,没有对现有公共接口的更改。 没有计划添加任何新的公共API或其他接口。🐱🐉
到目前为止完成的原型化工作证实了预期的内存占用减少、 GC 活动的大量减少以及在某些极端情况下的次要性能退化。🐱👓🐱🚀
总结起来就是使用 byte[] 能够比使用char[] 节省空间,减少GC活动
1.3、String不可变性
String:代表不可变的字符序列。简称:不可变性。
1、当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
public static void main(String[] args) { String str1 = "hello"; String str2 = "hello"; // 判断地址 这个时候肯定是 true 我们前文也讲了 采取字面量的定义方式,字符串会存储在公共池中 System.out.println(str1 == str2); }
public static void main(String[] args) { String str1 = "hello"; String str2 = "hello"; str1="abc,hao"; // 判断地址, 它由true -->false System.out.println(str1 == str2); }
通过字节码来看
2、当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
public static void main(String[] args) { String str1 = "hello"; String str2 = "hello"; str1+="abc,hao"; // 判断地址, 它由true -->false System.out.println(str1 == str2); }
从字节码文件可以看到,实际上所谓的连接字符,是通过StringBuilder.append()来执行的,之后再通过toString()方法返回回来。所以他们改变的也是原来的指向。
图的指向和第一个图差不多,为了省下篇幅, 就不画了哈。
3、当调用string的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
public static void main(String[] args) { String str1 = "hello"; str1=str1.replace("h","q"); }
通过字节码文件都可以明显看出来,对象是不同的。😃
4、通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
小结:通过上面几个小点,我想大家应该对这个是明白了吧。也能算证明了String的不可变性了吧。☺😁
注意:字符串常量池是不会存储相同内容的字符串的,相同的只会存储一份,上面的代码也体现出来了,目的是为了减少内存消耗
ldc 指令的意思,就是从常量池拿出一个 后面指令指向的东西。
二、String的内存分配
在Java语言中有8种基本数据类型和一种比较特殊的类型string。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,string类型的常量池比较特殊。它的主要使用方法有两种😶
- 直接使用双引号声明出来的String对象会直接存储在常量池中。 如 String info="我是宁在春";
- 如果不是用双引号声明的string对象,可以使用string提供的intern()方法。
public native String intern(); //当调用 intern 方法时,如果池中已经包含一个等于该String对象的字符串equals(Object)由equals(Object)方法确定equals(Object) ,则返回池中的字符串。 否则,将此String对象添加到池中并返回对此String对象的引用。
三、字符串拼接操作
- 常量与常量的拼接结果在常量池,原理是编译期优化
- 常量池中不会存在相同内容的变量
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
public static void main(String[] args) { //常量与常量的拼接结果在常量池,原理是编译期优化 String str1="hello"; // 此处肯定是存储在字符串常量池中的。 String str2="h"+"e"+"l"+"l"+"o"; // 这里你看图分析。 System.out.println(str1==str2); // true 因为存放在常量池中 System.out.println(str1.equals(str2)); // true }
在这里为什么说是常量池优化勒?我们来看这个class文件。
我们写的源代码在编译为.class文件时,"h"+"e"+"l"+"l"+"o" 就已经被编译器认为等同于”hello“,所以str2 实际上就是引用了字符串常量池中的 “hello”。
下面来看下面这道题:
@Test public void test() { String s1 = "Java"; String s2 = "Study"; String s3 = "JavaStudy"; String s4 = "Java" + "Study"; String s5 = s1 + "Study"; String s6 = "Java" + s2; String s7 = s1 + s2; // 请问 下面哪些是 true 哪些是false呢?? System.out.println(s3 == s4); System.out.println(s3 == s5); System.out.println(s3 == s6); System.out.println(s3 == s7); System.out.println(s5 == s6); System.out.println(s5 == s7); // 那么上面你都做对了 那下面这个勒? String s8 = s6.intern(); System.out.println(s3 == s8); }
答案是:
true,false,false,false,false,false,true
为什么勒?我们照常还是先来看看class文件。
s3== s4 很容易理解,他们编译完就是一样的。
为什么s3!=s5呢? 解释完这个后面都差不多。
s5=s1+"Stduy"
; 但是这一行代码,实际中间经过很多过程的。
s1+"Study" 实际是通过
StringBuilder.append()` 来添加的,最后再通过toString() 方法,再来返回一个对象的,深入进去StringBuilder.toString() 方法实际上就是 new String();
所以他们指向的位置是不同的。
String s8 = s6.intern();
System.out.println(s3 == s8); // 为true
源码上的注释讲的特别清楚
即:当调用 intern 方法时,如果池中已经包含一个等于该String对象的字符串equals(Object)由equals(Object)方法确定equals(Object) ,则返回池中的字符串。 否则,将此String对象添加到池中并返回对此String对象的引用。
四、intern()的使用
- intern是一个native方法,调用的是底层C的方法
- 字符串池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串对象相等的字符串,则返回池中的字符串。否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。
- 如果不是用双引号声明的string对象,可以使用string提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
如:
@Test public void test2() { String str1 = "i miss you"; String str2 = new String("i miss you").intern(); System.out.println(str1 == str2); // 结果为 true }
通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool😁
五、小小的几个面试题
也是当时好奇 (jdk 8
为背景讲的哈,之前的jdk 可能产生不一样的结果😊)。
1、 new String("ab")会创建几个对象?
1个还是2个呢? 真的是这样吗?你确定吗?
public static void main(String[] args) { String ab = new String("ab"); }
代码非常简单,从代码也看不出很多,我们打开字节码文件查看哈。
解析过程:
- 首先是创建了一个String对象,即new String(),new关键字在堆空间中创建一个String 对象。 即第一个对象。
- ”ab“,我们使用它的时候,会先去字符串常量池中寻找,发现没有,即在字符串常量池中创建。即第二个对象。
- 第三步就是 将堆中String的地址存储到局部变量ab中。
结论:所以答案是两个对象。
2、new String("a") + new String("b") 会创建几个对象
看这个你觉得是几个呢???三个? 四个还是五个?还是更多勒?或者是更少勒?
public static void main(String[] args) { String ab = new String("a") + new String("b"); }
依旧还是从字节码文件来看:
如果这样从字节码文件上看,确实只能看到五个,但是在上文中,我写了 StringBuilder.toString()
方法,它的底层就是调用 new String() ;
所以我们实际上是创建了 6个对象。
- 对象1:new StringBuilder()
- 对象2:new String("a")
- 对象3:常量池的 a
- 对象4:new String("b")
- 对象5:常量池的 b
- 对象6:toString中会创建一个 new String("ab")
- 调用toString方法,不会在常量池中生成ab
3、那么 new String("a"+"b")会创建几个对象勒???
在评论中给出答案哦。
六、自言自语
摸鱼的一天🧐,Java 也太卷了,学起来是真的累,努力的人特别努力,不努力的人瑟瑟发抖啊😔。
还是觉得躺平舒服🛌,一起来吧。