string的基本特性
- string:字符串,使用一对""来表示。
- String s1=“hello”;//字面量的方式定义
- String S2=new String(“hello”);
- string声明为final的,不可以被继承
- string实现了seriaizable接口,表示字符串是支持序列化的。实现了comparable接口:表示string是可以比较大小的
- string在jdk8及以前底层定义了 final char[] value用于存储字符串数据,jdk9时改为byte[]
一个char是两个字节,即16位,而一些常用字母和符号在byte中就可以存储下,用char来定义的话会浪费近一半的内存
- string:代表不可变的字符序列,简称:不可变性
- 当对字符串重新赋值时,需要重写指定内存区域进行赋值,不能使用原有的value赋值
- 当对现有字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
- 当调用string的replace()方法修改指定字符或者字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
- 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中
字符串常量池中是不允许存放相同的字符串的
- string的string pool是一个固定大小的hashtable,默认大小长度是1009,如果放进string pool的string非常多,就会造成hash冲突严重,从而导致链表会很长,而链表长了之后直接会造成的影响就是当调用string.intern时性能会大幅下降
- 使用-XX:StringTableSize可设置stringtable的长度
- 在jdk6中stringtable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快,stringtablesize设置没有要求
- 在jdk7中,stringtable的长度默认值是60013,
- jdk8开始,设置stringtable,1009是可设置的最小值
哈希碰撞的减少,可以提高性能
string的内存分配
- 在java语言中有8种基本数据类型和一种比较特殊的类型string,这些类型为了他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念
- 常量池就类似一个java系统级别提供的缓存,8中基本数据类型的常量池都是系统协调的,string类型的常量池比较特殊,它的主要使用方法有两种
- 直接使用双引号声明出来的string对象会直接存储在常量池中。比如 String info=“cxf”;
- 如果不是双引号声明的string对象,也可以使用string提供的intern()方法。这个后面重点谈
string的基本操作
先来看如下的代码
public class test {
public static void main(String[] args) {
System.out.println();
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");
}
}
使用idea的debug功能来逐行查看java.lang.string的count的变化
整体变化如行后注释
java语言规范里要求完全相同的字符串字面量,应该包含同样的unicode字符序列(包含一份码点序列的常量),必须是指向同一个string类实例,即相同的字符串只会在stringtable中加载一次
再看这段代码分析下内存指向
package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
int i=1;
Object obj=new Object();
test test = new test();
test.foo(obj);
}
private void foo(Object parm) {
String str=parm.toString();
System.out.println(str);
}
}
字符串拼接操作
-
常量与常量的拼接结果在常量池,原理是编译器优化 这里属于是在.java文件编译成.class文件时的优化,这里通过反编译后的字节码文件和一段代码来看 package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
String cxf="c"+"xf";
String cxf1="cxf";
System.out.println(cxf==cxf1);
System.out.println(cxf.equals(cxf1));
}
}
string的==同样是通过判断地址的 equals就不多了 重写的先地址后内容 我们再来看反编译后的字节码文件 -
常量池中不会存在相同的常量 这个在上边的字符串的基本操作中也有证明 -
只要其中有一个是变量,结果就在堆中。变量拼接的原理是stringbuilder 同样的来看一段代码和运行结果 package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
String s1="javaee";
String s2="hadoop";
String s3="javaeehadhop";
String s4="javaee"+"hadoop";
String s5=s1+"hadoop";
String s6="javaee"+s2;
String s7=s1+s2;
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);
System.out.println(s6==s7);
String s8=s6.intern();
System.out.println(s3==s8);
}
}
-
如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
string拼接的底层原理
从上边的代码测试中可以得出如果拼接符号的前后出现了变量。则相当于在堆空间new string(),但具体的底层细节又是怎么样的呢
先看这段代码:
package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
String str="cx";
String str1="f";
String str2="cxf";
String str3=str+str1;
System.out.println(str2==str3);
}
}
同样的可以看看反编译后的字节码
0 ldc #7 <cx>
2 astore_1
3 ldc #9 <f>
5 astore_2
6 ldc #11 <cxf>
8 astore_3
9 new #13 <java/lang/StringBuilder>
12 dup
13 invokespecial #15 <java/lang/StringBuilder.<init> : ()V>
16 aload_1
17 invokevirtual #16 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
20 aload_2
21 invokevirtual #16 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
24 invokevirtual #20 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
27 astore 4
29 getstatic #24 <java/lang/System.out : Ljava/io/PrintStream;>
32 aload_3
33 aload 4
35 if_acmpne 42 (+7)
38 iconst_1
39 goto 43 (+4)
42 iconst_0
43 invokevirtual #30 <java/io/PrintStream.println : (Z)V>
46 return
补充:在jdk5.0之后使用的是stringbuilder在jdk5.0之前使用的是stringbuffer
那么是不是所有的非字面量的拼接底层都是通过stringbuilder来进行的呢
同样的我们来看一段代码和对应的字节码文件
package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
final String s="cx";
final String s1="f";
String ss="cxf";
String s3=s+s1;
System.out.println(ss==s3);
}
}
从字节码指令中可以看出在编译期间,已经将其拼接好了,即编译优化
由此可以得出结论:
如果拼接字符串时,等号的两边都是常量引用或者直接就是字面量的拼接,则会在编译期间进行优化,直接进行拼接
final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用final就建议使用上
字符串拼接和append操作的效率对比
下边来看两端测试代码
package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
String cxf="";
long start = System.currentTimeMillis();
for (int i = 0; i <100000 ; i++) {
cxf+="a";
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:"+(end-start));
}
}
package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();
long start = System.currentTimeMillis();
for (int i = 0; i <100000 ; i++) {
stringBuilder.append("a");
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:"+(end-start));
}
}
每次进行string的拼接操作的时候,每次不仅会创建一个stringbuilder的对象,同样的stringbuilder.tostring还会创建一个string对象,而stringbuilder的操作只需要创建一个stringbuilder的对象,所以效率是远高于直接拼接的
同样的每次进行拼接都会创建对象,新的出现旧的就不在使用了,内存中创建了较多的stringbuilder和string的对象,一方面是内存的占用另一方面就是gc的话 还是要花费相应的时间
注意:
在实际的开发中,如果基本确定要前前后后添加的字符串长度不超过某个限定值highlevel的情况下,建议使用构造器实例化,避免不断的扩容,
stringbuilder s=new stringbuilder(highlevel);
intern()的使用
如果不是双引号声明的string对象,可以使用string提供的intern方法,intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
- 比如 string myinfo=new string(“i love cxf”).intern();
也就是说,如果任意字符串上调用string.intern方法,那么其返回结果所指向的哪个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true
(“a”+“b”+“c”).intern()==“abc”
通俗点讲,interned string 就是确保字符串在内存中只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意这个值会被存放在字符串内部池(string intern pool)
如何保证变量s指向的是字符串常量池中的数据呢?
有两种方式
方式一:
直接以字面量的形式定义
string s=“cxf”
方式二:
string s=new string(“cxf”).intern();
面试题
1.题目 new string(“ab”)会创建几个对象?
两个
一个对象是:new关键字在堆空间创建的
另一个对象是:字符串常量池中的对象 字节码指令ldc
2.new string(“a”)+new string(“b”)呢?
对象1:new stringbuilder()
对象2:new string(“a”)
对象3:常量池"a"
对象4:new string(“b”)
对象5:常量池中 的"b"
深入剖析: stringbuilder的tostring()
? 对象6:new string(“ab”)
? 强调以下tostring的调用,在字符串常量池中,没有生成"ab"
同样的如下代码进行分析
package xx.cxf.feifei.test;
public class test {
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,首先在创建的过程中会在堆中创建string对象,同样的也会在常量池中创建“1”对象,指向方向是s指向堆中的对象,堆中的对象在指向常量池中的对象,s.intern(); 此时常量池中是存在
“1”的返回的就是常量池的对象地址,但此时并没有接收,s指向的仍然是堆中的string,此时s指向的堆中的对象,s2指向的是常量池中的“1”,所以结果为false
而对于s3和s4的比较
首先s3的指向是最后的stringbuilder.tostring后的对象,此时常量池中存在"1" ,stringbuilder.tostring的对象是不会在常量池中的,所以常量池中并没有“11”,调用s3的intern()将“11”,可能加载进常量池,此时常量池中有“11”了,但注意此时的“11”并不是真的“11”,他只是stringbuilder.tostring的返回值的引用,为了堆空间的高效利用具体可查看如下的图,这个是1.7的,jdk1.6的则不相同,后面会叙述
同样的巩固下,看下面的代码,你是否能判断分析正确
package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
String s3=new String("1")+new String("1");
String s4="11";
String s5 = s3.intern();
System.out.println(s3==s4);
System.out.println(s4==s5);
}
}
同样的在进行完拼串操作后s3指向的是stringbuilder.tostring方法返回的string,而"11"是没有加载进常量池的,此时s4通过字面量赋值,"11"对象加载进常量池,且s4指向常量池的11,s3.intern()f返回s4的指向地址,并将它返回给s5,所以答案如题中的注释
总结string的intern()的使用
- jdk1.6,将这个字符串对象尝试放入串池
- 如果串池中有,则并不会放入,返回已有的串池的对象的地址
- 如果没有,会把此对项复制一份放入串池,并返回串池中的对象地址
- jdk1.7起,将这个字符串对象尝试放入串池
- 如果串池中有,则并不会放入串池。
- 如果串池中没有,则会将对象的引用地址复制一份,放入串池,并返回串池中的引用地址
懂了这个过后再来看到例题吧
package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
String s=new String("a")+new String("b");
String s2=s.intern();
System.out.println(s2=="ab");
System.out.println(s=="ab");
}
}
对于jdk1.6来说:
s.intern()是将“ab”加载进常量池,s2指向的是ab,而s指向的是堆中的string对象所以输出为 true false
对于jdk1.8来说:
s.intern()是将s指向的对象的地址的引用复制,放入堆中,所以输出为true true
stringtable的垃圾回收
-XX:+PrintStringTableStatistics 开启stringtable的维护明细
如下测试代码
package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
}
}
维护有1774个字符串
package xx.cxf.feifei.test;
public class test {
public static void main(String[] args) {
for(int i=0;i<100000;i++){
String.valueOf(i).intern();
}
}
}
字符串的数量明显低于我们添加进stringtable的
同样也发生了ygc即minor gc
G1的string去重操作
字符串常量池本身就不存在重复的字符串
这里是对堆中的string对象即数组对象来去重的 实现
- 当垃圾收集器工作的时候,会访问堆上存活的对象,对每一个访问的对象都会检查是否是候选的要去重的string对象
- 如果是,把这个对象的一个引用插入到队列中等待后续的处理,一个去重的线程在后台运行,处理这个队列,处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它所引用的string对象
- 使用一个hashtable来记录所有的被string对象使用的不重复的char数组,当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组
- 如果存在,string对象会被调整引用那个数组,释放对原来数组的引用,最终会被垃圾收集器回收掉
- 如果查找失败,char数组会被插入到hashtable,这样以后就会被共享
|