案例引入
- 案例映入
public static void main(String[] args) {
String s1="a";//放入常量池
String s2=new String("a");//创建对象放入堆中
String s3="b";//放入常量池
String s4="a"+"b";//将“ab”放入常量池
String s5="ab";//前面s3已经放入常量池,这里直接从常量池取就行
String s6=s1+s3;//调用StringBuilder方法
String s7=s6.intern();
System.out.print("s1==s2--");
System.out.println(s1 == s2);//false
System.out.print("s4==s5--" );
System.out.println(s4 == s5);//true
System.out.print("s5==s6--");
System.out.println(s5 == s6);//false
System.out.print("s5==s7--");
System.out.println(s5==s7);//true
}
这里的原因需要先了解JVM相关的知识,大家可以先看一下,了解的话可以直接跳到后面
JVM虚拟机结构基础知识
我们先来说一下虚拟机的基础知识:
- 堆:
- 通过new关键字创建的对象都会存储在堆中。
- 所有线程共享的区域,(注意线程安全问题)
- 有垃圾回收机制。
- 程序技术器
JVM程序计数器 - 虚拟机栈
是线程运行时需要的内存空间,每个栈内由多个栈帧组成,每个方法运行时需要的内存为栈帧。
则栈的定义是
- 每个线程运行时所需要的内存称为虚拟机栈。
- 每个栈由多个栈帧组成,栈帧对应每次方法调用所需要的虚拟机内存。
- 每个线程只能有一个栈帧,对应当前正在执行的方法。
- 本地方法栈
本地方法:指不是由Java语言编写的代码,一般用native修饰,我们直接是无法查看源码的。
例如:Object的clone方法:
- 方法区
不同的Jvm厂商和不同的JDK版本,JVM会不同,上图是JDK1.8的JVM堆和方法区结构。
我们看到1.8里方法区被放到了本地内存(1.6方法区就不在本地内存中),而方法区的实现叫做云空间,它也是被被所有线程共享的,里面有类的信息,构造函数,常量池。 而比较特殊的是StringTable(串池)在1.8中在是在堆中,而在1.6中串池在常量池中,常量池在堆中。作为扩展下图是1.6JVM结构。
案例解析
String s1="a";//放入常量池
String s2=new String("a");//创建对象放入堆中
String s3="b";//放入常量池
String s4="a"+"b";//将“ab”放入常量池
String s5="ab";//前面s3已经放入常量池,这里直接从常量池取就行
String s6=s1+s3;//调用StringBuilder方法
String s7=s6.intern();
-
s1s2
String s1=“a”;//放入StringTable串池 String s2=new String(“a”);//创建对象放入堆中
System.out.println(s1s2);这句代码输出结果是false,这是因为,当我们通过String s1=“a”,那么S1创建对象将a放入串池中。而S2明确new创建了对象当然是放在堆中。我们知道==比较对象存储的地址,所以两个地址当然不同输出当然是false。 -
这里我们还要说一点,当我们在写下面这句代码之前,我们只是将a放入常量池,当我们类加载后会把a放入运行中常量池,这时它只是运行中常量池中的一个符号,还没有变为Java字符串对象放入串池。当后面我们写了下面这句代码明确创建s1字符串对象才会给串池中添加a。
String s1="a";
- s4s5
s4就是直接当做String s4="ab"处理,也是将"ab"放入串,所以,我们看到s4s5为true,两个地址相同。String s5=“a”+“b”;是Java在编译期间的优化,结果已经在编译期间确定,所以不需要使用StringBuilder来操作。
我们看看下面的字节码就可以看出
String s5=“a”+“b”;
public static void main(String[] args) {
String s1="a";
String s2="b";
String s4="ab";
String s5="a"+"b";
System.out.println(s4==s5);
}
- s5==s6
String s6=s1+s3
String s6=s1+s3 完成了怎样的操作
得到字节码文件如下,我并没有全复制完只是到String s6=s1+s3能解释它进行了怎样的操作
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: new #4 // class java/lang/StringBuilder**(创建一个StringBuilder对象)**
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder.""😦)V (StringBuilder对象构造函数)
13: aload_1 (获取a)
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;(掉用StringBuilder.append方法,将a拼接到StringBuilder对象)
17: aload_2 (获取b)
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
(掉用StringBuilder.append方法,将b拼接到StringBuilder对象)
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
(掉用StringBuilder.toString方法,输出StringBuilder对象)
24: astore_3 将StringBuilder结果存入s3
所以String s6=s1+s3 对应的代码相当于 String s3=new StringBuilder().append(s1).append(s2).toString()
JVM默认使用编译器优化,使用创建StringBuilder()对象,在使用append方法来实现字符串拼接,当然创建了对象那就是在堆区,不在常量区,和s5内存地址自然不相同。这里我们还要注意new String和new StringBuilder两个对象还是不一样的
所以但我们要经常改变字符串大小,那么就不能使用String直接相加,因为底层会创建StringBuilder对象,太损耗空间,当我们需要线程安全时想要改变字符串,就更不能用String的相加了,因为StringBuilder是线程不安全的,后面我会详细赘述。
后面我会专门讲StringBuilder()和另外一个StringBuffer()。这两个方法实现原理相同,只是一个是线程安全,一个线程不安全。而且String和StringBuilder()两个对象也不相同,后面会详细赘述。
- String s7=s6.intern();
intern方法作用是将还不存在于串池中的字符串放入串池。
String,StringBuffer与StringBuilder的区别
- 概念
1、用来处理字符串常用的类有3种:String、StringBuffer和StringBuilder
2、三者之间的区别:
都是final类,都不允许被继承;
String类长度是不可变的,StringBuffer和StringBuilder类长度是可以改变的;
StringBuffer类是线程安全的,StringBuilder不是线程安全的;
- String 和 StringBuilder:
1、String类型和StringBuilder类型的主要性能区别:String是不可变的对象,因此每次在对String类进行改变的时候都会生成一个新的string对象,然后将指针指向新的string对象,所以经常要改变字符串长度的话不要使用string,因为每次生成对象都会对系统性能产生影响,特别是当内存中引用的对象多了以后,JVM的GC就会开始工作,性能就会降低;
2、使用StringBuilder类时,每次都会对StringBuilder 对象本身进行操作,而不是生成新的对象并改变对象引用,所以多数情况下推荐使用StringBuilder,特别是字符串对象经常要改变的情况;
3、在某些情况下,String对象的字符串拼接其实是被Java Compiler编译成了StringBuilder对象的拼接,所以这些时候String对象的速度并不会比StringBuilder对象慢
- StringBuilder和StringBuffer
StringBuilder是5.0新增的,此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同
使用策略
1、基本原则:如果要操作少量的数据,用String ;单线程操作大量数据,用StringBuilder ;多线程操作大量数据,用StringBuffer。
2、不要使用String类的”+”来进行频繁的拼接,因为那样的性能极差的,应该使用StringBuffer或StringBuilder类,这在Java的优化上是一条比较重要的原则,
当出现上面的情况时,显然我们要采用第二种方法,因为第一种方法,每次循环都会创建一个String result用于保存结果,除此之外二者基本相同
3、 StringBuilder一般使用在方法内部来完成类似”+”功能,因为是线程不安全的,所以用完以后可以丢弃。StringBuffer主要用在全局变量中
4、相同情况下使用 StirngBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。而在现实的模块化编程中,负责某一模块的程序员不一定能清晰地判断该模块是否会放入多线程的环境中运行,因此:除非确定系统的瓶颈是在 StringBuffer 上,并且确定你的模块不会运行在多线程模式下,才可以采用StringBuilder;否则还是用StringBuffer