平时只知道使用String,却不知道String有这么多奥秘,今天就来好好探讨一下。
String概述
我们从源码下手
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
首先看到String是一个final的类,甚至所有的变量都是final修饰,final有什么用呢?
- final:final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的。
在Java.lang包中的很多类都是被声明为final,目的是为了保证基础功能不被修改,保证平台的安全。
很多人看到上面的说法会认为final修饰的类或者变量时不可变的,这是错误的,final不等于immutable,举个简单的例子:final List strList =new ArrayList.....只能保证list这个引用不可以被修改,但不能保证list的对象行为(关于对象行为,面向对象我在这里面有说过,不在赘述)
不可变性
image.png- 实现原理:
- final修饰类和成员变量
我们知道包括String是一个典型的immutable类,显然final是为了实现这个目标而来的,但是仅仅是final是不够的,就像我们上面说的List一样,String内部维护了一个char数组用来存储字符串中的字符,我可以对数组元素做修改啊。 - private成员变量
/** The value is used for character storage. */
private final char value[];
String类没有暴露任何内部成员字段 - 没有任何setter方法
String提供的方法中没有任何一个可以让我们修改value字段 - 构造器实现了深拷贝
- final修饰类和成员变量
/**
* Allocates a new {@code String} so that it represents the sequence of
* characters currently contained in the character array argument. The
* contents of the character array are copied; subsequent modification of
* the character array does not affect the newly created string.
*
* @param value
* The initial value of the string
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
以上所说的是String成为了一个immutable类,这也是我们以后设计不可变类的一些方式
- 不可变的好处
其实简单来说就两个字:安全
举个简单的例子:在hashmap或者hashset中,使用String做键,如果使用可变类做键,那么可容易就破坏键值的唯一性
public class Test {
public static void main(String[] args) {
HashSet<String> strings = new HashSet<>();
String s = "1";
strings.add(s);
String s1 = s;
s1 += "123";
System.out.println(strings);
HashSet<StringBuilder> stringBuilders = new HashSet<>();
StringBuilder sb = new StringBuilder("1");
stringBuilders.add(sb);
StringBuilder sb1 = sb;
sb1.append("123");
System.out.println(stringBuilders);
}
}
image.png
使用可变类的时候键值被修改,这是很大的问题
第二个例子:多并发的场景下,使用可变类需要额外的同步并且很容易出问题,但是不可变类就不存在这些问题(为啥?不可变类的状态只有一种,并且由构造函数来控制,所以是无法修改的,也就不存在线程安全问题)
- 性能:
在我们的应用程序中,String是大量被使用的,如果重复的创建大量的对象,必然会造成性能的下降,这里也是String使用final的目的之一,对于重复的字符串对象,使用常量池来避免大量的对象创建和销毁。
String常量池
在上一篇关于Java内存区域管理的文章中大概的说了一下方法区,在Java8中,String常量池移入了堆中 image.png大概是这个样子,详细的我就不再画了。
常量池是干啥的?
很明显,用来缓存String对象的呗,复用。
String常量可能会在两种时机进入常量池:
- 编译期:通过双引号声明的常量(包括显示声明、静态编译优化后的常量,如”1”+”2”优化为常量”12”),在前端编译期将被静态的写入class文件中的“常量池”。该“常量池”会在类加载后被载入“内存中的常量池”,也就是我们平时所说的常量池。同时,JIT优化也可能产生类似的常量。
引用网上的一些例子:
使用 ” ” 双引号创建 : String s1 = “first”;
使用字符串连接符拼接 : String s2=”se”+”cond”;
使用字符串加引用拼接 : String s12=”first”+s2;
使用new String(“”)创建 : String s3 = new String(“three”);
使用new String(“”)拼接 : String s4 = new String(“fo”)+”ur”;
使用new String(“”)拼接 : String s5 = new String(“fo”)+new String(“ur”);
image.png
我们看到所有的字面量被放进了常量池,这里要注意的是下面这个例子:
String result = "hello" + "world" ;
常量池中是helloworld
String a="hello";
String b="world";
String result =a+b;
常量池中是hello和world
final String a="hello";
final String b="world";
String result =a+b;
常量池中是hello和world 以及helloworld
- 运行期:调用String#intern()方法,可能将该String对象动态的写入上述“内存中常量池”
但是,在Java6之后,由于SCP内存位置的变化,intern也发生了变化String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
//这段代码更加直观
// String s3 = new String("1") + new String("1");
// System.out.println(s3 == s3.intern(););
}
这里我用的jdk8做的测试 结果为false和true
image.png
String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。接下来String s2 = "1"; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。
String s3 = new String("1") + new String("1");,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String("1")我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。
接下来s3.intern();这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,关键点是 jdk76以后常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
public static void main(String[] args) {
//intern下调
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
}
image.png
运行结果为false,false,为啥呢,String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。。接下来String s2 = "1";这一句是 s2 对象去常量池中寻找后发现 “1” 已经在常量池里了 ,所以s2引用的是常量池的对象,s.intern这句代码已经没有作用了,因为常量池中已经存在1了。 结果就是 s 和 s2 的引用地址明显不同
String s3 = new String("1") + new String("1");,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String("1")我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。然后 String s4 = "11";在常量池中生成了11对象,所以s4这时候是指向常量池中的对象的,s3.intern也没有任何影响,因为常量池中已经存在11对象了
最后总结一下就是:String#intern 方法时,如果常量池中存在对象,则返回常量池中的对象;否则,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。(常量池中既有对象也有引用)
- intern注意事项
JAVA 使用 jni 调用c++实现的StringTable的intern方法, StringTable的intern方法跟Java中的HashMap的实现是差不多的, 只是不能自动扩容。默认大小是60013(Java8)。
要注意的是,String的String Pool是一个固定大小的Hashtable,默认值大小长度是60013,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。
可以使用下面命令查看你的StringTable大小
-XX:+PrintStringTableStatistic...
网友评论