Java详解【String】+【StringBuilder vs StringBuffer】+【字符串拼接】

String详解

  • 注意区分对象和对象的引用

首先来看一下我在jdk中找到的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所修饰的,因此String类对象不可变,也不可继承。这里要注意一个误区字符串对象不可变,但字符串变量所指的值是可变的,即引用地址可变。String变量存储的是对String对象的引用,String对象里存储的才是字符串的值【注意区分对象和对象的引用】。看下面的例子

String str = "abc"; //str是个对象引用
System.out.println(str); //输出abc
str = "abcde"; //不会出错
System.out.println(str); //输出abcde

当给str第二次赋值的时候,对象"abc"并没有被销毁,仍存放在常量池中(String自带),只是让str指向了"abcde"的内存地址,而字符串对象"abcde"是新生成的,即str第二次赋值时并不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。记住,对String对象的任何改变都不影响到原对象,相关的任何改变的操作都会生成新的对象。

 

  • String对象是否真的不可变?!

答案并不是的,那么该如何做呢?那就是使用反射(反射不太懂的可以点击链接学习——反射基础详解

从上面的源码可知String的成员变量是private final的,也就是初始化之后不可改变。那么在这几个成员中,value比较特殊,因为它是一个引用变量,而不是真正的对象。value是final修饰的,即final不能再指向其他数组对象,那么我想改变value指向的数组时,比如将数组中的某个位置上的字符变为下划线"_"。因为不能直接通过这个引用去修改数组,那么为了访问到至value这个引用,我们可以使用反射来访问私有成员,反射出String对象中的value属性,进而改变value数组的内容。

public static void testReflection() throws Exception {
    // 创建字符串"Hello World" 并赋给引用s
    String s = "Hello World";
    System.out.println("s = " + s); // 打印出Hello World
    // 获取String类中的value字段————Field类获取成员变量
    Field valueFieldOfString = String.class.getDeclaredField("value");
    valueFieldOfString.setAccessible(true); // 改变value属性的访问权限
    value[5] = '_'; // 获取s对象上的value属性的值 改变value所引用的数组中的第5个字符
    System.out.println("s = " + s); // 打印出Hello_World
}

(此例子参考了博文:https://blog.csdn.net/zhangjg_blog/article/details/18319521

 

下面这个例子将引出两个问题

  1. String str="hello world" 和 String str=new String("hello world") 有什么不同
  2. 为什么使用 "==" 和 "equals" 会有不同的结果
public static void main(String[] args) {
    String str1 = "hello world";
    String str2 = new String("hello world");
    String str3 = "hello world";
    String str4 = new String("hello world");
    String str5 = "Hello" + " World";
    String str6 = "Hello" + new String(" World");
    String str7 = str2.intern();
    String ex1 = "Hello";
    String ex2 = " World";
    String str8 = ex1 + ex2;
         
    System.out.println(str1 == str3);       //true
    System.out.println(str1 == str2);       //false
    System.out.println(str2 == str4);       //false
    System.out.println(str1.equals(str2));  //true
    System.out.println(str2.equals(str4));  //true
    System.out.println(str1 == str5);       //true
    System.out.println(str1 == str6);       //false
    System.out.println(str1 == str7);       //true
    System.out.println(str1 == str8);       //false
}

【运行结果】是 true、false、false、true、true、true、false、true、false

先把下面四大点看懂了,我会在最后写出【解析】
 

  • String的两种赋值方式

区分【String str="HW"】和【String str=new String("HW")】

(1)字面量赋值方式     eg:String str = "Hello";

  • 该种直接赋值的方法,JVM会去字符串常量池(String对象不可变)中寻找是否有equals("Hello")的String对象,如果有,就把该对象在字符串常量池中"Hello"的引用复制给字符串变量str,如若没有,就在堆中新建一个对象,同时把引用驻留在字符串常量池中,再把引用赋给字符串变量str。
  • 用该方法创建字符串时,无论创建多少次,只要字符串的值(内容)相同,那么它们所指向的都是堆中的同一个对象。
  • 该方法直接赋值给变量的字符串存放在常量池

(2)new关键字创建新对象     eg:String str = new String("Hello");

  • 利用new来创建字符串时,无论字符串常量池中是否有与当前值相同的对象引用,都会在中新开辟一块内存,创建一个新的对象
     

注意:对字符串进行拼接操作,即做"+"运算的时候,分2种情况:

  1. 表达式右边是纯字符串常量,那么存放在常量池里面。eg:String str = "Hello" + "World";
  2. 表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面。eg:String str = str1 + str2;

总结:常量池是方法区的一部分,而方法区是线程共享的,所以常量池也是线程共享的,且它是线程安全的,它让有相同值的引用指向同一个位置,如果引用值变化了,但是常量池中没有新的值,那么就会新建一个常量结果来交给新的引用,对于同一个对象,new出来的字符串存放在中,而直接赋值给变量的字符串存放在常量池里。
​​​​​​

  • 字符串比较——区分"=="和"equals"

"==":比较引用变量的地址,即两个对象是否引用同一地址的内容,用"=="时会检测是否指向同一个对象

"equals":比较对象的内容,即两个对象内容上是否相同

字符串用这两种比较方式都是可行的,具体看想要比较什么,总体来看,"=="稍微强大些,因为它既要求内容相同,也要求引用对象相同
 

  • intern() 方法

当使用 intern() 方法时,会先查询字符串常量池是否存在当前字符串,如果存在,则返回常量池中的引用,若不存在,则将字符串添加到字符串常量池中,并返回字符串常量池中的引用。

 

【代码解析】

str1 == str3——str1与str3指向常量池中同一个对象,引用对象相同,因此用"=="比较时结果为true
str1 == str2——new会创建一个新的对象放在堆中,str1所指对象在常量池,即使str1与str2内容相同,但并不是同一个对象
str2 == str4——new会创建一个新的对象放在堆中,str2与str4指向不同的对象,即使内容相同
str1.equals(str2)——str1于str2各自引用对象不同,但内容相同,因此用"equals"比较时结果为true
str2.equals(str4)——str2于str4各自引用对象不同,但内容相同,因此用"equals"比较时结果为true
str1 == str5——对字符串进行拼接操作("+"运算)时,表达式右边是纯字符串常量,那么存放在常量池里面,若常量池中有该字符串,则返回该引用
str1 == str6——对字符串进行拼接操作("+"运算)时,表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面
str1 == str7——intern()方法会去常量池中找是否存在当前字符,存在则返回引用,该对象引用刚好是str1所指向的
str1 == str8——str8由两个变量拼接,编译期不知道它们的具体位置,所以不会做出优化,必须要等到运行时才能确定,因此新对象的地址和前面的不同。

 

StringBuilder  & StringBuffer

StringBuilder和StringBuffer类类似于String类,但区别在于String创建的对象是不可改变的,而StringBuilder和StringBuffer这两个类创建对象后都是可以对对象进行修改的。即String为字符串常量,而StringBuilder和StringBuffer均为字符串变量

  • 运行速度(快到慢):StringBuilder > StringBuffer > String

StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行像String对象那样子进行创建和回收的操作,所以速度要比String快很多。

  • 在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全

如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。

 

总结

  • String:适用于少量的字符串操作的情况
  • StringBuilder:适用于线程下在字符缓冲区进行大量操作的情况
  • StringBuffer:适用线程下在字符缓冲区进行大量操作的情况

 

字符串拼接五种方法

  • 使用 +
  • 使用 concat
  • 使用 StringBuilder
  • 使用 StringBuffer
  • 使用 StringUtils.join

注:先看完上面对String、StringBuilder和StringBuffer的详解后再看下面的文章会好理解很多的

由于String是Java中一个不可变的类,所以他一旦被实例化就无法被修改,因此所有的所谓字符串拼接,都是重新生成了一个新的字符串。

  • 效率(用时短到长):StringBuilder < StringBuffer < concat < < StringUtils.join

(1)"+"是Java提供的一个语法糖,而使用+拼接的字符串,它将String转成了StringBuilder后,再使用StringBuilder.append进行处理。如果不是在循环体中进行字符串拼接的话,直接使用+就好了。

(2)concat方法,其实是new了一个新的String

(3)StringUtils.join也是通过StringBuilder来实现的

(4)StringBufferStringBuilder的基础上,做了同步处理,所以在耗时上会相对多一些。

(5)如果在并发场景中进行字符串拼接的话,要使用StringBuffer来替代StringBuilder。因为StringBuilder是线程不安全的,而StringBuffer是线程安全

更多具体的请参考这篇文章:https://blog.csdn.net/hollis_chuang/article/details/86505501(推荐——字符串拼接)

 

其他参考文章:

https://www.cnblogs.com/su-feng/p/6659064.html

https://www.cnblogs.com/dolphin0520/p/3778589.html

https://www.cnblogs.com/justcooooode/p/7603381.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章