3. Java 字符串

实际上任何语言都没有提供字符串这个概念,而是使用字符数组来描述字符串。Java 里面严格来说也是没有字符串的,在所有的开发里面字符串的应用有很多,于是 Java 为了应对便创建了 String 类这个字符串类。使用 "" 定义的内容都是字符串,理解 Java 的 String 类需要从类的角度和内存关系上分析这个类。

3.1. String

3.1.1. String 的定义

注意一个常见的错误,不要记错了。因为 String 是 final 修饰的,无法被继承。所以 String 不是 Java 的基本数据类型。

字符串在 Java 中是不可变的,因此适合在多线程环境下使用。

代码块 3.1.1 String 的定义
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
代码块 3.1.2 String 字符串的存储方式
/** The value is used for character storage. */
private final char value[];

3.1.2. String类对象的实例化方式

代码块 3.1.3 String类对象的实例化方式
String name1 = "Sakura";    //直接赋值方式
String name2 = new String("Sakura");  //利用构造方法实例化

当我们使用双引号创建一个字符串时,如上 name1,JVM 首先在字符串池中寻找具有相同值的字符串。如果找到了,它将返回字符串池中的字符串对象的引用。否则,它会在字符串池中创建字符串对象并返回引用。JVM 通过在不同的线程中使用相同的字符串,节省了大量的内存。

如果使用 new 运算符创建字符串,则会直接在堆中创建它。

3.1.3. String 类对象运算

3.1.3.1. 使用"=="和equals比较字符串是否相等

代码块 3.1.4 ==和equals比较字符串是否相等
String name1 = "Sakura";
String name2 = new String("Sakura");
System.out.println("name1==name2 :"+ (name1 == name2));
System.out.println("name1.equal(name2) : "+name1.equals(name2));
System.out.println("name=='Sakura' : "+ (name1=="Sakura"));
System.out.println("name1.equal("sakura") :" +name1.equals("sakura") );
/** code running result :
    *
    * name1==name2 :false
    * name1.equal(name2) : true
    * name=='Sakura' : true
    * name1.equal(sakura) :false
    */

使用"=="比较的是两个对象在内存中的地址是否一致,也就是比较两个对象是否为同一个对象。 使用equals()方法比较的是对象的值是否相等,

两个字符串只有在它们具有相同字符串的时候才相等, equals() 方法区分大小写。如果您正在寻找不区分大小写的检查,您应该使用 equalsIgnoreCase() 方法。

3.1.3.2. 字符串拼接

Java中不允许程序员重载任何操作符,但是Java内部重载了两个用于String类的操作符"+“和”+=“。操作符”+“可以用于连接字符串,操作符”+="用于将连接后的字符串再次赋给原字符串引用。尽管在内部它使用 StringBuilder 来执行这个动作。

由于 String 在 Java 中是不可变的,因此每当我们执行字符串拼接操作时,它都会生成一个新的 String 并丢弃旧的 String 以进行垃圾收集。

备注

这些重复的操作会在堆中产生大量垃圾冗余。所以 Java 提供了 StringBuffer 和 StringBuilder 类,用于字符串操作。StringBuffer 和 StringBuilder 是 Java 中的可变对象。

代码块 3.1.5 字符串拼接测试代码
public static void main(String[] args) {
    String name1 = "Sakura";
    String name2 = new String("Sakura");
    name1=name1+name2;
}
代码块 3.1.6 反编译字符串拼接测试代码(javap -c)
public static void main(java.lang.String[]);
Code:
    0: ldc           #2                  // String Sakura
    2: astore_1
    3: new           #3                  // class java/lang/String
    6: dup
    7: ldc           #2                  // String Sakura
    9: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
    12: astore_2
    13: new           #5                  // class java/lang/StringBuilder
    16: dup
    17: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
    20: aload_1
    21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    24: aload_2
    25: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    28: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    31: astore_1
    32: return

我们通过反汇编测试代码的 class 文件,然后我们可以看到在 main 方法中的行编号为13的地方新建了一个 StringBuilder 对象,而且通过之后的代码可知,字符串的拼接运算是通过 StringBuilder.append 方法来执行的。

备注

javap 是 Java class文件分解器,可以反编译(即对javac编译的文件进行反编译),也可以查看java编译器生成的字节码;用于分解class文件。javap 的可选选项可以通过命令 javap -help 了解。 关于 javap 的更多信息 点击查看 javap 命令 笔记

3.2. StringBuffer 和 StringBuilder

它们为字符串操作提供了 append、insert、delete 和 substring 方法。

StringBuffer

StringBuilder

线程安全

非线程安全

同步

非同步

始于 Java 1.0

始于 Java 1.5

在 Java 1.4 之前,StringBuffer 是字符串操作的唯一选择。但是,它的一个缺点是所有公共方法都是同步的。 StringBuffer 提供线程安全性,但以性能为代价。

在大多数情况下,我们不会在多线程环境中使用 String。假设在单线程环境中或无关线程安全,要使用 StringBuilder。反之,使用 StringBuffer 进行线程安全的操作。

3.3. 总结

  • String 是不可变的,而 StringBuffer 和 StringBuilder 是可变类。

  • StringBuffer 是线程安全和同步的,而 StringBuilder 不是。这就是 StringBuilder 比 StringBuffer 快的原因。

  • 字符串连接运算符 (+) 在内部使用 StringBuilder 类。

  • 对于非多线程环境中的字符串操作,我们应该使用 StringBuilder 否则使用 StringBuffer 类。