Java-String
文章目录- 1.String[^1]简介
- 1.1.验证String是不可变对象
- 1.2.两种创建字符串对象的区别
- 1.3.创建了几个对象
- 1.3.1.情况1
- 1.3.2.情况2
- 1.3.3.情况3
- 1.3.4.情况4
- 2.String实现
- 2.1.不可变如何实现
- 3.可变的字符串
基于Oracle JDK11和JVM 11
1.String1简介
Java数据类型分两种:基本数据类型和引用数据类型。关于基本数据类型,可以参考我这篇博文Java极简入门教程——3.基本数据类型和变量_编程还未的博客-CSDN博客
除了八个基本数据类型之外,其他的都是引用数据类型。八个基本数据类型中,并没有包含字符串。Java提供了一个类,来储存字符串类型的值。自然而然,类叫 String。
String类表示字符串。Java程序中的所有字符串文本(例如“Hello World”)都是String类的实例(实例既对象)。
String有几个特点:
- String类是不可变的;它的值在创建后无法改变。
- 针对常量池的优化。
- final修饰
- javac 编译器可以根据 JDK 版本使用 StringBuffer、StringBuilder 或 java.lang.invoke.StringConcatFactory 实现运算符(+)
前三个比较重要。
String使用有两种方式,隐式创建和显示创建。
隐式创建
String abc = "张三";
显示创建
String abc = new String("李四");
1.1.验证String是不可变对象
学习一个方法System.identityHashCode()
,打印对象的hash码值,这个值和hashCode()
返回的一样。
先看一小段代码
String abc = "张三";
System.out.println(abc);
abc = "李四";
System.out.println(abc);
输出结果:
张三
李四
对于这段代码,会不会觉得String对象从“张三”变成了“李四”?有过有此疑问的话。我再放一点代码,验证一下。
String abc = "张三";
System.out.println(abc);
//System.identityHashCode 打印对象的hash码值
System.out.println(System.identityHashCode(abc));
abc = "李四";
System.out.println(abc);
//System.identityHashCode 打印对象的hash码值
System.out.println(System.identityHashCode(abc));
//打印第二遍是为了证明同一个对象的hash码值不会因为多次打印而改变。
System.out.println(System.identityHashCode(abc));
结果如下:
张三
1915910607
李四
284720968
284720968
是不是很震惊。abc指向的对象不一样了。这就是Java中String的特性,不可变对象。
我们解释一下刚才的代码,写在注释里面
//创建一个String对象,字符串文本值是“张三”,
//声明String类型的引用abc,指向值是“张三”的String对象
String abc = "张三";
System.out.println(abc);
//引用指向 值是“李四”的对象。之前值是“张三”的对象并没改变,
//而是创建了一个新对象,abc指向了新对象
abc = "李四";
System.out.println(abc);
注释看完后,这两行代码代码在内存中的变化看下图。关于JVM运行时数据区不太懂的,可以参考我这篇博文面试必备——图文剖析JVM的运行时数据区_编程还未的博客-CSDN博客
为了更好的理解,把我关于JVM运行时数据区的部分内容,贴过来。
Java会为每个类和接口维护一个运行时常量池(run-time constant pool )。类或接口的二进制表示形式(字节码)中的常量池表用于在创建类或接口时构造运行时常量池。
运行时常量池中所有引用最初都是符号引用。这些符号按照如下方式,从类或接口的二进制表示形式(字节码)中得出:
其中一项是字符串常量。字符串常量是指向String类实例的引用,它来自于类或接口二进制表示(字节码)中的CONSTANT_String_info结构。CONSTANT_String_info结构给出了由Unicode码点序列组成的字符串常量。Java语言规定,相同的字符串常量必须指向同一个String实例。2
下面两项是正确的:
- 如果以前在包含与结构给定的相同的 Unicode 码位序列的类实例上调用了该方法,则字符串常量是同一类实例的一个。
- 否则,将创建一个新的类实例,其中包含结构给出的 Unicode 码位序列。字符串常量获取的结果是指向那个新String实例的引用。最后,新String实例的intern方法被虚拟机自动调用。
原文如下:
- A string constant is a
reference
to an instance of classString
, and is derived from aCONSTANT_String_info
structure. To derive a string constant, the Java Virtual Machine examines the sequence of code points given by theCONSTANT_String_info
structure:
- If the method
String.intern
has previously been invoked on an instance of classString
containing a sequence of Unicode code points identical to that given by theCONSTANT_String_info
structure, then the string constant is areference
to that same instance of classString
.- Otherwise, a new instance of class
String
is created containing the sequence of Unicode code points given by theCONSTANT_String_info
structure. The string constant is areference
to the new instance. Finally, the methodString.intern
is invoked on the new instance.
总结一下上面的内容,得到两点:
- 运行时常量池中有字符串常量,字符串常量是指向字符串实例的引用。
- 相同的字符串常量必须指向同一个String实例。
基于上面两点,以及对象在堆中(不了解这个的,可以看一下我的博文面试必备——图文剖析JVM的运行时数据区_编程还未的博客-CSDN博客)。
【字符串常量池在堆中】这个观点,在JVM规范中并无体现。至于HotSpot有没有用【堆中的字符串常量池】实现上述总结的两点,不确定。
我们画一下String的内存示意:
String abc = "张三";
abc = "李四";
由此可以看出,“张三”对象还在,abc指向了“李四”对象。
内存图+hashCode码值,为String类是不可变的; 提供支持。
1.2.两种创建字符串对象的区别Java的String有两种创建对象的方式。
第一种
String abc = "张三";
第二种
String cba = new Stgring("李四");
第一种,会在方法区的运行时常量池的字符串常量中寻找,是否有对象“张三”的引用,有就直接指向;没有就在对创建对象“张三”,把“张三”对象的引用保存到运行时常量池的字符串常量中,abc再指向字符串常量中的引用。
第二种,堆创建对象,检查运行时常量池的字符串常量,有没有指向“李四”对象的引用。如果有,刚才new String()创建的对象,指向字符串常量中“李四”的引用;如果没有,就在堆中创建“李四”对象,“李四”对象的引用保存到字符串常量中,cba指向字符串常量。
放两种情况的字节码,注释放在字节码中。字节码指令官方3
第一种字节码
//String abc = "张三";
LINENUMBER 9 L0
LDC "\u5f20\u4e09"
ASTORE 1
第一种字节码带注释
//String abc = "张三";
//源码的行数 第9行
LINENUMBER 9 L0
//LDC 从运行时常量池推送到顶(Push item from run-time constant pool),如果是字符串常量,则把值推出来
//"\u5f20\u4e09"是“张三”的Unicode
LDC "\u5f20\u4e09"
//ASTORE 存储到局部变量中(Store into local variable reference)
ASTORE 1
第二种字节码
//String cba = new Stgring("李四");
LINENUMBER 11 L0
NEW java/lang/String
DUP
LDC "\u674e\u56db"
INVOKESPECIAL java/lang/String. (Ljava/lang/String;)V
ASTORE 1
第二种字节码带注释
//String cba = new Stgring("李四");
//源码的行数 第11行
LINENUMBER 11 L0
//NEW 创建对象(Create new object)
NEW java/lang/String
//DUP 复制顶部 *** 作数堆栈值(Duplicate the top operand stack value)
DUP
//LDC 从运行时常量池推送到顶(Push item from run-time constant pool),如果是字符串常量,则把值推出来
//"\u674e\u56db"是“李四”的Unicode
LDC "\u674e\u56db"
//INVOKESPECIAL 调用实例方法;直接调用实例初始化方法和当前类及其超类型的方法
//(Invoke instance method; direct invocation of instance initialization methods and methods of the current class and its supertypes)
INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
//ASTORE 存储到局部变量中(Store into local variable reference)
ASTORE 1
第一种的内存示意图
String abc = "张三";
第二种的内存示意图
1.3.创建了几个对象 1.3.1.情况1String abc = "张三";
0个对象或1个对象。
- 0个对象:如果字符串常量池中有“张三”的话,abc直接指向字符串常量池的“张三”;
- 1个对象:如果字符串常量池中没“张三”的话,先创建对象,对象存储的内容是“张三”,把“张三”放到字符串常量池。
内存情况,比较简单跟上面的图一样,不重复了。
1.3.2.情况2String abc = "张三";
String def = "张三";
//打印 abc 的hash码值
System.out.println(System.identityHashCode(abc));
//打印 def 的hash码值
System.out.println(System.identityHashCode(def));
0个对象或1个对象。分析同情况1。
这里只分析String abc = "张三";
就可以了。程序的运行结果如下:
1915910607
1915910607
abc的hash码值和def的hash码值是一样的,能证明指向的是同一个对象。
或者用比较也可以,结果是true。==
比较对象的地址值,equals
比较的是字面量。
String abc = "张三";
String def = "张三";
System.out.println(abc==def);
为什么出现这种情况?def是在干嘛?
JVM的机制,会把String的值,比如“张三”保存到运行时常量池的字符串常量中,方便后续使用,而不重复创建对象,节省开销。这个在上面也讲过了,是Java语言规范的规定。
def发现字符串常量中有指向“张三”对象的引用,直接指向字符串常量。内存情况如下
1.3.3.情况3String abc = "张三";
String hig = new String("张三");
System.out.println(System.identityHashCode(abc));
System.out.println(System.identityHashCode(hig));
输出结果是如下
1915910607
284720968
说明abc和hig指向的不同的对象。
abc那行代码:0个或1个,分析跟上面情况1一样。
hig那行代码:1个对象。new String()
肯定会创建1个对象,因为已有“张三”对象的引用在运行时常量池的字符串常量中,new String()会保存字符串常量的引用。
内存分析如下:
1.3.4.情况4String abc = "张三";
String klm = new String("李四");
System.out.println(System.identityHashCode(abc));
System.out.println(System.identityHashCode(klm));
运行结果
1915910607
284720968
abc那行代码:0个或1个,分析同情况1
klm那行代码:1个或2个。new String()
肯定会创建1个对象。“李四”如果在运行时常量池的字符串常量中,就不会在新创建对象了;没有的话就创个对象,把引用存到运行时常量池的字符串常量中。最后都是字符串常量中的引用保存到刚new String()的对象中。所以是1个或2个。
内存分析如下:
2.String实现这是Java中最常用的一个类,如果看过Java内存的话,会发现占用内存最多的就是char[](Java8及之前)或者byte[](Java8及之后),这也能看出String的重要性。怎么看出来的呢?
看下面源码:
/**
* The value is used for character storage.
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*
* Additionally, it is marked with {@link Stable} to trust the contents
* of the array. No other facility in JDK provides this functionality (yet).
* {@link Stable} is safe here, because value is never null.
*/
@Stable
private final byte[] value;
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
* @param original
* A {@code String}
*/
@HotSpotIntrinsicCandidate
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
private final byte[] value;
用来存储。public String(String original) {}
构造方法,也是把值存到value。
因为Java中使用是String是最多的,String底层是byte[],所以通常JVM内存镜像中,byte[]占用最多内存。
2.1.不可变如何实现先讲一下final4。这里只提两点:
- 修饰属性:只能在初始化时赋值,且只能赋值一次,赋值后值无法改变。
- 修饰类:被修饰的类无法被继承。
上面提到过,String底层用private final byte[] value
来存值,这个value是byte[]数组,初始化一次后不可再变;且并没有提供方法类修改这个byte[]内部的值;private
修饰,只能在本类中使用;final修饰不可被继承,没子类重写里面的方法。
最主要的是:初始化时赋值一次,不提供任何修改底层存储值的方法。
3.可变的字符串要想使用可变字符串,Java提供了StringBuilder
、StringBuffer
,可以使用这个类。
这俩类的主要区别是:StringBuffer
是线程安全的、速度慢一些,StringBuilder
是非线程安全的,速度快一些。
引用资料:Java11语言规范、JVM11规范、JDK11源码 ↩︎
String Literals ↩︎
Instructions ↩︎
final ↩︎
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)