浅析Java中的String、StringBuffer和StringBuilder

173 阅读9分钟

文章目录


1. 概念

Java中的String类用于字符串的创建以及有关字符串的一系列操作,程序中所有的字符串字面值都是String类的对象。字符串具有以下特点:

  • 字符串是不可变的:String类定义和保存数据的char型数组都被final关键字所修饰:

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    
    	...
    }
    
  • 字符串是可共享的:由于字符串是不可变的,因此即使被共享使用也无法被修改

  • 字符串效果上相当于char[]字符数组,但底层原理是byte[] 数组


2. 创建方式

字符串的创建方法有很多,String对应的构造方法如下所示:


在这里插入图片描述

其中构造方法所能接受的参数有byte数组、char数组、codePoints的int数组、已有的字符串、StringBuffer和StringBuilder。常用的主要是四种:直接创建和其中三种构造方法

  • 直接创建:这也是使用最多的一种方法,如String s = "Hello World";

  • 构造方法

    • public String():创建一个空白字符串,不包含任何内容
    • public String(char[] array):根据字符数组的内容创建对应的字符串
    • public String(byte[] array):根据字节数组的内容创建字符串
 public class StringTest {
     public static void main(String[] args) {
         // 三种构造
         String str1 = new String();
         System.out.println(str1);  // ""
 
         char [] array = new char []{'k', 'o', 'b', 'e'};
         String str2 = new String(array);
         System.out.println(str2); // "kobe"
 
         byte[] bytearray = new byte[]{97, 98, 99};
         String str3 = new String(bytearray); // "kobe"
         System.out.println(str3);
 
         // 直接创建
         String str4 = "kobe";
         System.out.println(str4); // "kobe"
 
     }
 }

3. 字符串常量池

3.1 字符串创建

程序中直接写上双引号的字符串都在字符串常量池中,那么它们和使用构造方法创建的字符串有什么不同呢?首先通过一个例子直观的看一下:

public class StringTest1 {
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";

        char[] chararray = new char[]{'a', 'b', 'c'};
        String str3 = new String(chararray);
        System.out.println(str3);

        System.out.println(str1 == str2); // true
        System.out.println(str1 == str3); // false
        System.out.println(str2 == str3); // false
    }
}

如上所示,str1str2通过双引号直接创建的,str3通过构造方法创建。然后我们使用== 来比较它们发现,str1str2是相同的,但它们与str3都是不同的。表面上看起来它们都创建了字符串"abc",为什么会出现这种情况呢?

这是因为,通过双引号直接创建的字符串都保存在堆中的字符串常量池中,而通过构造方法创建的字符串在堆内存空间中。对于基本类型来说,==是进行数值的比较,而对于引用类型来说是进行地址值的比较。下面我们通过内存空间来看一下为什么会有这种现象。


在这里插入图片描述

代码如上所示,接下来我们按照代码的执行过程依次看以下内存空间是如何变化的:

  • str1str2是通过""直接创建的,它们实际都指向了保存在堆内存的字符串常量池中的"abc"的地址,假设为0x233
  • 接着创建一个char型数组,数组内容为['a', 'b', 'c'],它保存在堆中
  • 然后通过构造方法创建一个String对象,它同样保存在堆中,同时假设它在堆中的地址为0x666,因此栈中str3保存的就是对象的地址0x666。但底层仍然是一个byte[]

由上可知,由于str1str2指向的是字符串常量池中同样的内容,因此地址相同,==的结果为true。而str3是new的一个新的String对象,它保存在堆的其他位置而不是在字符串常量池中,因此地址不同,==的结果为false。


由上面的分析知道,== 进行的是地址值的比较。如果想要进行字符串内容的比较,可以使用以下两个方法:

  • public boolean equals(Object obj): 参数可以是任何对象,只有参数是一个字符串并且内容相同时返回true,否则返回false
  1. 任何对象都能用Object接收
  2. equals方法具有对称性,即a.equals(b)b.equals(a) 效果一样
  3. 如果比较一个常量和一个变量,推荐使用 常量.equals(变量)的写法
  • public boolean equalsIgnoreCase(String str): 忽略大小写,进行内容比较

    public class StringTest2 {
        public static void main(String[] args) {
            String str1 = "abc";
            String str2 = "abc";
    
            char[] chararray = new char[]{'a', 'b', 'c'};
            String str3 = new String(chararray);
    
            System.out.println(str1.equals(str2));  // true
            System.out.println(str1.equals(str3));  // true
            System.out.println(str3.equals("abc")); // true
            System.out.println("abc".equals(str1)); // true
    
            String strA = "Java";
            String strB = "java";
            System.out.println(strA.equals(strB)); // false
            System.out.println(strA.equalsIgnoreCase(strB)); // true
        }
    }
    

下面再来看一下其他创建字符串的方法,以及对应的内存空间的变化情况。例如,此时代码如下所示:

@Test
public void test(){
      char[] chararray = new char[]{'a', 'b', 'c'};
      String s = new String(chararray);
      System.out.println(s);  // "abc"

      System.out.println(chararray.hashCode() == s.hashCode());  // false
  }

那么它们在内存中是如何存放的呢?如下所示:


在这里插入图片描述

虽然s的字符数组和chararray数组的内容是相同的,但它们并不是在相同的空间中进行存放。另外,如果使用Strings = new String(chararray, 0, 1)创建字符串,那么s保存的方式同样是在堆中创建了一个新的字符数组。

最后再来看一下String s = "abc";String s1 = new String("abc");在内存空间中的情况,如下所示:


在这里插入图片描述

String s1 = new String("abc");首先在堆中创建了一个String对象,但对象指向的仍然是字符串常量池中的"abc"

3.2 字符串拼接

接着看一下字符串的拼接操作在内存空间中是如何变化的,首先我们来从代码入手,如下所示:

@Test
public void test(){
     String s1 = "hello";
     String s2 = "world";
     String s3 = "hello" + "world";
     String s4 = s1 + "world";
     String s5 = s1 +s2;
     String s6 = (s1 +s2).intern();

     System.out.println(s3 == s4); // false
     System.out.println(s3 == s5); // false
     System.out.println(s4 == s5); // false
     System.out.println(s3 == s6); // true

 }

如何理解上述代码的输出呢?下面通过图示的方法来看一下:
在这里插入图片描述
s1s2都是直接创建的字符串字面值,因此,对应的"hello""world"都直接保存在字符串常量池中。s3进行的是字符串的拼接操作,它本身并不涉及对象的操作,因此拼接后的字符串"helloworld"也是保存在字符串常量池中。s4s5都涉及String对象的操作,虽然对象中保存的数据内容相同,但是分别指向的是堆中不同的String对象。s6首先进行字符串的拼接,结果字符串是"helloworld",但是调用intern()知道它已经在字符串常量池中存在,因此直接将其赋给s6,并不会创建新的字符串常量。


4. 常用方法

4.1 String中和获取相关的方法:

  • public int length(): 获取字符串中含有的字符个数

  • public String concat(String str): 将当前字符串和参数字符串拼接为新字符串返回,当然同样可以通过 + 实现字符串拼接

  • public char charAt(int index): 获取指定索引位置的单个字符

  • public int indexOf(String str): 查找参数字符串在本字符串当中首次出现的索引位置,如果没有返回-1

    public class StringGetTest {
        public static void main(String[] args) {
    
            String str = "abc";
            System.out.println(str.length()); // 3
    
            String str1 = "hello";
            String str2 = "world";
            String str3 = str1.concat(str2);
            System.out.println(str1); // hello
            System.out.println(str2); // world
            System.out.println(str3); // helloworld
    
            System.out.println(str.charAt(0)); // a
    
            System.out.println(str.indexOf("bc")); // 1
            System.out.println(str.indexOf("ff")); // -1
        }
    }
    

4.2 字符串的截取方法:

  • public String subString(int index):截取从参数位置一直到字符串末尾,返回新字符串

  • public String subString(int begin, int end) :截取[begin, end)范围内的字符串

    public class SubStringTest {
        public static void main(String[] args) {
            String str = "hello world";
    
            String str2 = str.substring(2);
            System.out.println(str2); // llo world
            System.out.println(str.substring(2, str.length() - 2)); // llo wor
    
            // strA 中保存的是地址值
            // 为strA 赋予不同的字符串是改变了strA中保存的地址值,但"hello"和"world"是不可改变的
            String strA = "hello";
            System.out.println(strA); // hello
            strA = "world";
            System.out.println(strA); // world
        }
    }
    

4.3 字符串转换相关方法

  • public char[] toCharArray(): 将当前字符串拆分为字符数组返回

  • public byte[] getBytes(): 获取当前字符串底层的字节数组

  • public String replace(charSequence oldString, CharSequence newString): 将所有出现的老字符串替换成新的字符串,返回替换之后的结果

    public class StringConvertTest {
        public static void main(String[] args) {
            String str = "helloworld";
    
            char [] charArray = str.toCharArray();
            System.out.println(charArray); // helloworld
            System.out.println(Arrays.toString(charArray)); // [h, e, l, l, o, w, o, r, l, d]
            System.out.println(charArray.length);  // 10
    
            byte [] byteArray = str.getBytes();
            System.out.println(byteArray[3]); // 108
    
            String str1 = "Fologen love Kobe";
            String str2 = str1.replace("o", "*");
            System.out.println(str1);  // Fologen love Kobe
            System.out.println(str2);  // F*l*gen l*ve K*be
    
        }
    }
    

4.4 字符串分割方法

  • public Sting[] split(String regex):按照参数的规则,将字符串切分成若干部分,其中regex为正则表达式

    public class StringSplitTest {
        public static void main(String[] args) {
            String str = "aaa,bbb,ccc";
            String [] array = str.split(",");
            for (int i = 0; i < array.length; i++) {
                System.out.println(array[i]); // aaa  bbb ccc
            }
        }
    }
    

5. 更多

Java String 类

Class String


6. StringBuffer

StringBuffer用于表示可变的字符序列,对于使用StringBuffer声明的字符串内容增删时,不会产生新的对象。StringBuffer的定义如下:

public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {
    private transient char[] toStringCache;
    static final long serialVersionUID = 3388685877147921107L;
    private static final ObjectStreamField[] serialPersistentFields;

    public StringBuffer() {
        super(16);
    }

    public StringBuffer(int var1) {
        super(var1);
    }

    public StringBuffer(String var1) {
        super(var1.length() + 16);
        this.append(var1);
    }

    public StringBuffer(CharSequence var1) {
        this(var1.length() + 16);
        this.append(var1);
    }

    public synchronized int length() {
        return this.count;
    }

    public synchronized int capacity() {
        return this.value.length;
    }

	...
}

类中所定义的方法都被synchronized关键字所修饰,因此,它是线程安全的。但是synchronized所带来的线程同步开销,使得StringBuffer的操作效率较低。

StringBuffer提供了四个构造器,定义如下:

public StringBuffer() {
	super(16);
}

public StringBuffer(int var1) {
    super(var1);
}

public StringBuffer(String var1) {
    super(var1.length() + 16);
    this.append(var1);
}

public StringBuffer(CharSequence var1) {
    this(var1.length() + 16);
    this.append(var1);
}

其中无参构造器默认使用初始容量为16的字符串缓冲区;StringBuffer(int var1)用于构造指定容量的字符串缓冲区;StringBuffer(String var1)用于将内容初始化为指定字符串内容。

常用的方法有:

  • append(xxx)
  • delete(int start, int end)
  • replace(int start, int end, String str)
  • insert(int offset, xxx)
  • reverse()
  • indexOf(String str)
  • subString(int start, int end)
  • charAt(int n)
  • serCharAt(int n, char ch)

方法的使用见名知意,这里不多解释,详细可用可参考API文档

值得注意的一点就是,当使用append()或是insert()时,如果原来的value数组长度不够,那么StringBuffer会进行扩容。


7. StringBuilder

StringBuilder和StringBuffer非常类似,也可以用来表示可变字符序列,不同之处在于它是线程不安全的,但操作效率较高。

public class StringBuilderMain {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        System.out.println(sb); // ""

        sb.append("Forlogen");
        sb.append("kobe");
        sb.append(24);
        System.out.println(sb);  // Forlogenkobe24

        StringBuilder sb1 = new StringBuilder("Forlogen");
        System.out.println(sb1); // Forlgoen
        System.out.println(sb1.toString());  // Forlogen

        System.out.println(sb1.length());  // 8
        System.out.println(sb1.insert(3, "KOBE")); // ForKOBElogen

    }
}