文章目录
String
- String实现了
Serializable
接口:表示字符串是支持序列化的。 - 实现了
Comparable
接口:表示String可以比较大小 - String声明为
final
的, 不可被继承 - String在jdk8及以前内部定义了
final char value[]
,value用于存储字符串数据,jdk9时改为byte[]
String的不可变性
- 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
- 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
- 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
字符串常量池
String str="i"与 String str=new String(“i”)一样吗?
- String str="i"会将起分配到常量池中,常量池中没有重复的元素,如果常量池中存中i,就将i的地址赋给变量,如果没有就创建一个再赋给变量。
- String str=new String(“i”)会将对象分配到堆中,即使内存一样,还是会重新创建一个新的对象。
-
通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
//字面量 String a = "aa";
-
如果不是用双引号声明(字面量方式)的String对象,可以使用String提供的
intern()
方法 -
字符串常量池中是不会存储相同内容的字符串的
-
String Pool 是一个固定大小的
Hashtable
,默认值大小长度是1009。如果放进StringPool的String非常多, 就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String. intern
时性能会大幅下降。 -
使用
-XX:StringTableSize
可设置StringTable的长度 -
在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设 置没有要求
-
在jdk7中,StringTable的长度默认值是60013
-
jdk8开始,1009是StringTable长度可设置的最小值
-
Java 6及以前,字符串常量池存放在永久代
-
Java 7中Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内
1、永久代默认情况下比较小
2、永久代的回收效率较低,垃圾回收频率低
- 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样在进行调优应用时仅需要调整堆大小就可以了
- 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用
String. intern()
-
Java8元空间,字符串常量在堆
String拼接操作
- 常量与常量的拼接(+)结果在常量池,原理是编译期优化
- 常量池中不会存在相同内容的常量。
- 只要其中有一个是变量,结果就在堆中,变量拼接的原理是***StringBuilder***
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
- 注意字符串拼接操作不一定使用StringBuilder
- 情况一:拼接符号左右两边都是字符串常量:
"a"+"b"
- 情况二:拼接符号左右两边都是常量引用:
final String a= "a";final String b="b"; a+b;
- 情况一:拼接符号左右两边都是字符串常量:
@Test
public void test1(){
String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
/*
* 最终.java编译成.class,再执行.class
* String s1 = "abc";
* String s2 = "abc"
*/
System.out.println(s1 == s2); //true
System.out.println(s1.equals(s2)); //true
}
@Test
public void test2(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";//编译期优化
//如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
System.out.println(s6 == s7);//false
//intern():判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop的地址;
//如果字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回次对象的地址。
String s8 = s6.intern();
System.out.println(s3 == s8);//true
}
'+'和append的区别
append的方式比拼接字符串更加高效
-
使用append的方式,从始至终只需要创建一个stringbuilder对象
-
使用字符串拼接,每次都需要创建stringbuilder对象、String对象,占用内存过多
-
比如在循环体里面:
String str = ""; for(int i=0; i<10000;i++){ str += i; }
每一条语句,都会生成一个新的StringBuilder,多到没谱!
-
StringBuilder和StringBuffer,字符串是存放在
char[]
中的,char[]
是存放在堆中的。
相比String每次+都重新创建一个String对象,重新开辟一段内存不同,StringBuilder和StringBuffer的append都是直接把String对象中的char[]的字符直接拷贝到StringBuilder和StringBuffer的char[]上,效率比String的+高得多。当然,当StringBuilder和StringBuffer的char[]长度不够时,也会重新开辟一段内存。 -
StringBuilder的内部有一个char[], 在调用StringBuilder的无参构造方法时其内部char[]的默认长度是16。当我们调用StringBuilder的append方法时,其实就是不断的往char[]里填东西的过程。
-
StringBuilder默认长度是16,然后,如果要append第17个字符,怎么办?
答案是采用 Arrays.copyOf()成倍复制扩容!
扩容的性能代价是很严重的:一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64, 128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。
由此可见,合理设置一个初始值多重要。使用之前先仔细评估一下要保存的字符串最大长度。
所以,在实际开发中,如果基本确定需要添加字符串的长度,就可以自定义长度实例化stringbuilder
StringBuilder a = new StringBuilder(10);
- StringBuffer和StringBuilder的功能基本一样,只是StringBuffer是线程安全的,而StringBuilder不是线程安全的。因此,StringBuilder的效率会更高。
intern()方法
public native String intern();
1、如果不是用双引号声明的String对象,可以使用String提供的intern
方法: intern
方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
- 比如:
String myInfo = new String("I love u").intern();
- 也就是说,如果在任意字符串上调用String. intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。
2、因此,下列表达式的值必定是true:
("a" + "b" + "c").intern()== "abc";
通俗点讲,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)。
new String会创建几个对象?
-
new String(“ab”)会创建几个对象?
-
两个;看字节码就行。
0 new #2 <java/lang/String> 3 dup 4 ldc #3 <ab> 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V> 9 astore_1 10 return
-
1:new决定的:在堆空间中分配一块内存,创建一个对象;
-
2:字符串常量池里面放一个ab。
-
-
new String(“a”) + new String(“b”)会创建几个对象?
0 new #2 <java/lang/StringBuilder> 3 dup 4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V> 7 new #4 <java/lang/String> 10 dup 11 ldc #5 <a> 13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V> 16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> 19 new #4 <java/lang/String> 22 dup 23 ldc #8 <b> 25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V> 28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> 31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> 34 astore_1 35 return
-
new StringBuilder();
-
new String(“a”)
-
常量池里面的a
-
new String(“b”)
-
常量池里面的b
-
StringBuilder的toString()方法:new String(“ab”)
toString()方法的调用,在字符串常量池中,没有生成"ab"
-
相关面试题
/**
* 如何保证变量s指向的是字符串常量池中的数据呢?
* 有两种方式:
* 方式一: String s = "shkstart";//字面量定义的方式
* 方式二: 调用intern()
* String s = new String("shkstart").intern();
* String s = new StringBuilder("shkstart").toString().intern();
*/
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
String s1 = s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
String s2 = "1";
//s 指向堆空间"1"的内存地址
//s1 指向字符串常量池中"1"的内存地址
//s2 指向字符串常量池已存在的"1"的内存地址 所以 s1==s2
System.out.println(s == s2);//jdk6:false jdk7/8:false
System.out.println(s1 == s2);//jdk6: true jdk7/8:true
System.out.println(System.identityHashCode(s));//491044090
System.out.println(System.identityHashCode(s1));//644117698
System.out.println(System.identityHashCode(s2));//644117698
//s3变量记录的地址为:new String("11")
String s3 = new String("1") + new String("1");
//执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
// jdk6:创建了一个新的对象"11",也就有新的地址。
// jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址
s3.intern();
//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
String s4 = "11";
System.out.println(s3 == s4);//jdk6:false jdk7/8:true
}
}
拓展
public class StringIntern1 {
public static void main(String[] args) {
String s3 = new String("1") + new String("1");//new String("11")
//执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
String s4 = "11";//在字符串常量池中生成对象"11"
String s5 = s3.intern();//去常量池里面找有没有"11",没有的话就指向那个引用,有的话就使用有的
System.out.println(s3 == s4);//false
System.out.println(s5 == s4);//true
}
}
总结String中Intern()的使用
jdk6中,将这个字符串对象尝试放入串池
- 如果串池中有,则不会放入,返回已有的地址
- 如果没有,将此字符串对象复制一份放入,并返回对象地址
jdk7后,将这个字符串对象尝试放入串池
- 如果有,不会放入,返回已有的地址
- 如果没有,将字符串对象的引用地址复制一份放入,返回引用地址
public class StringExer1 {
public static void main(String[] args) {
String s = new String("a") + new String("b");//new String("ab")
//在上一行代码执行完以后,字符串常量池中并没有"ab"
String s2 = s.intern();//jdk6中:在串池中创建一个字符串"ab"
//jdk7后:串池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回
System.out.println(s2 == "ab");//jdk6:true jdk7后:true
System.out.println(s == "ab");//jdk6:false jdk7后:true
}
}
public class StringExer1 {
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");//new String("ab")
String s2 = s.intern();
System.out.println(s2 == x);//true
System.out.println(s == x);//false
}
}
public class StringExer2 {
public static void main(String[] args) {
String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成"ab"
// String s1 = new String("a") + new String("b");执行完以后,不会在字符串常量池中会生成"ab"
s1.intern();
String s2 = "ab";
System.out.println(s1 == s2); //false
}
}
1、两个String拼接,因为最后stringBuilder调用了toString方法,所以返回的是一个new String()地址
2、如果常量池里面没有拼接后的值,调用intern方法,常量池中放的是new String()的地址
3、如果常量池有拼接后的值,调用intern方法,就会返回常量池这个值的地址
intern()空间效率
大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用 intern()方法,就会明显降低内存的大小。
G1中的String去重操作
- 背景:对许多Java应用(有大的也有小的)做的测试得出以下结果:
- 堆存活数据集合里面String对象占了25%
- 堆存活数据集合里面重复的String对象有13.5%
- String对象的平均长度是45
- 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面**,Java堆中存活的数据集合差不多25%是String对象**。更进一步,这里面差不多一半String对象是重复的,重复的意思是说:
string1. equals (string2)=true
。堆上存在重复的string对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。
实现
- 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。
- 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
- 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。 当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。
- 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
- 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。
命令行选项
- UseStringDeduplication (bool) :开启String去重,默认是不开启的,需要手动开启。
- PrintStringDedupl icationStatistics (bool) :打印详细的去重统计信息,
- StringDedupl icationAgeThreshold (uintx) :达到这个年龄的string对象被认.为是去重的候选对象
今日推歌
----《从前说》
从前对妈妈说谎
翻山越岭只为能与你拥抱一场
幻想 披上红妆
后来你娶了理想 我嫁给了户对门当
爱与不爱又何妨
从前说无惧山海
用九百九十九张车票换有你的未来
只要你在
后来你把我归还人海
哭着说欠我的未来
下辈子再爱