1.intern方法
intern()方法可以在运行期间向字符串中动态加入字符串实例的方式,它的功能很简单,总结起来就一句话
可以在运行时向字符串池中添加字符串常量
添加的原则是,如果常量池中存在当前字符串,则直接返回常量池中它的引用;如果常量池中没有此字符串,则将此字符串的引用放入常量池,然后返回这个引用。
字符串进入常量池有两个途径:
- 1.字面量在编译器会进入Class的常量池,在类加载后会进入运行时常量池
- 2.使用
intern()
底层实现
String#intern()方法在JVM中是通过JNI调用C++实现的,其实里面调用的C++当中的StringTable的intern()方法,它的内部结构和HashMap类似,但是它不能扩容,默认大小是1009
如果字符串常量池的String非常躲,就会造成Hash冲突,从而导致链表会很长,它的查询性能将会从O(1)变成O(n),当调用intern方法时性能将会下降
在JDK6的版本中大小是固定的,在JDK7中可以通过参数来设置-XX:StringTableSize=12345
举个例子
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);
}
- s == s2 // false
s和s2比较分析:
String s = new String("1");
new String(“1”)同时创建了两个对象
一个对象是常量池中的"1"
一个是堆中的String对象
由于是new出来了的对象,s指向的是堆中的引用
s.intern();
调用intern方法,如果常量池中存在该字符串,则返回常量池引用,
如果不存在则将此字符串的引用加入到常量池中,然后返回
此时 常量池中已经有"1"这个字符串了,不需要再向常量池中添加,
所以这个地方返回的是常量池中的引用,另外返回的这个常量池的引用也并没有赋值给其他变量
String s2 = "1";
s2创建了一个"1"的字符串对象,这个时候会向StringTable(常量池)中查询是否存在该字符串,如果存在则返回这个引用,注意这个引用是常量池中的引用
System.out.println(s == s2);
s指向堆中的引用
s2指向的是常量池中的引用
在JDK6中,字符串常量池是放在Perm区域的,也就是放在方法区当中
方法区中的引用和堆中的引用,两者是属于不同的区域,必然是不相等的false
在JDK7中,字符串常量池移动到了堆中,原因是方法区的容量相比堆空间比较小,存储不了太多的常量,不过并不影响结果,仍然是两个区域内的对象进行比较仍然是false
- s3和s4比较分析:
String s3 = new String("1") + new String("1");
s3在这里创建了两个对象,一个是堆中的"1"字符串对象,另一个两个new String(“1”) 拼接起来的"11"字符串对象放入到了堆中,但常量池中是没有这个"11"的
s3指向的是堆中的"11"对象
s3.intern();
接着s3调用intern方法,将"11"字符串对象放入到了字符串常量池中
在JDK6和JDK7的版本处理是不同的
在JDK6中,是复制堆中的字符串对象添加到字符串常量池中
在JDK7中,是复制堆中的字符串对象的引用添加到字符串常量池中
String s4 = "11";
s4在创建"11"字符串对象时,会先在StringTable中查询一番,如果有则返回常量池中的引用,如果没有则添加进去
在前面s3.intern()
步骤中,由于已经将"11"放入到了字符串常量池中,所以这里返回的是常量池中的引用
System.out.println(s3 == s4);
由于intern()方法在不同的JDK版本里面会有差异,所以它们的比较结果也是不同的
在JDK6中,s4指向的是常量池中引用(堆对象的副本),由于内存区域不一样,所以为false
在JDK7中,s4指向的是常量池中引用(堆对象的引用),s4虽然指向的也是常量池中的引用,但是常量池中存储的这个引用是堆对象的引用,所以两者在比较时是一样的,所以为true
思考例子
下面还有一段代码,各位可以再思考下,结果是怎样的
public static void main(String[] args) {
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);
}
2.String有没有最大长度限制?
大家都用过String字符串,有的人可能还不知道它的长度在某些方面是有一些限制。
public String(byte bytes[], int offset, int length);
这是java.lang.String
中的一个构造函数,可以看到它的长度是int类型,int的最大取值是2^31-1.但是我们却不能认为String支持的最大长度是这么大,这个长度的范围是JVM在运行期对String的一种限制,并非是编译器定义字符串的时候的限制。因为JVM是有StringTable
字符串常量池这个概念的,如果编译器里面允许的最大长度那就是int类型的最大范围,大家知道这意味这什么吗,这意味着一个String字符串可以支持4G长度大小了,这怎么可能允许?
String s = "111111.....1111";// 其中有10万个字符"1";
当执行javac编译时会抛出异常,IDEA会提示你常量字符串过长
底层实现
你可能很好奇为什么不是int类型的长度限制。这其实跟Java虚拟机规范有关,当我们按照String s= "xxx"的形式定义字符串时,xxx被我们称为字面量,这种字面量在编译之后会以常量的形式进入Class常量池,既然要进入常量池,就需要遵循常量池的有关规定
《Java虚拟机规范》官方链接
https://docs.oracle.com/javase/specs/jvms/se7/html/index.html
根据《Java虚拟机规范》中的4.4节定义,CONSTANT_String_info
用于表示java.lang.String
类型的常量对象,格式:
CONSTANT_String_info{
u1 tag;
u2 string_index;
}
其中,string_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Uft8_info结构,表示一组Unicode字符绪列,这组Unicode字符序列最终会被初始化为一个String对象
CONSTANT_Uft8_info结构用于表示字符串常量的值
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
其中length指明了bytes[]数组的长度,其类型为u2
根据《Java虚拟机规范》,u2表示2字节的无符号数,1字节有8位,2字节有16位,而16位无符号数可以表示的最大值为2^16-1=65535
也就是说,Class文件中常量池的格式规定了其字符串常量的长度不能超过65535
String s = "11111...11"; //其中有65535个1
但是编译时,仍然会报错,还是常量字符串过长,有的人可能会说不是说最大时65535吗?为什么这里还会报错,这个原因可以在javac的代码中可以找到Gen类中有如下代码
private void checkStringConstant(DiagnosticPosition var1, Object var2) {
if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
this.log.error(var1,"limit.string", new Object[0]);
++this.nerrs;
}
}
上面就是编译期的限制了,运行期的限制就是int类型的4GB的大小限制了,我们可以通过以下代码实现
String s ="";
for (int = 0; i < 100000; i++) {
s+= "" +i;
}