深入理解String、StringBuffer与StringBuilder

String、StringBuffer与StringBuilder

String

String是Java中处理字符串最常见的类,然而比较特别的是,String是被final修饰的,并且String中用来存储字符串的char数组value也是final的,并且是平private的,这意味着用户不仅不可以改变value的引用,也没有办法修改value的值,因此String是一个不可变类,在每次对String对象进行更改的时候,例如对String进行拼接,都会生成一个新的对象,然后将指针指向新的String对象。

String为什么被设计成final类型?
  • 字符串常量池的要求
  • 安全性:String在Java中被广泛应用于类的参数,例如网络请求,打开文件等操作,若String是可变的,随意更改String对象的内容可能会引起连接或文件被更改,进而导致严重的安全威胁
  • 此外我还看到一个比较有意思的说法是,Java并不是操作系统本地语言,并不能直接与操作系统交互,而需要借助本地方法,那么像String这样的核心类,其中便有一些方法需要借助操作系统本地API完成,如果这些核心类可以被继承并重写方法,很有可能对操作系统带来威胁

虽然String相关的方法均没有加任何锁,但由于它的不可变性,使得String对象可以在多个线程之间自由共享

字符串常量池

字符串常量池的设计意图

既然String类是不可变的,那么每次在对String进行更改时,都会创建出新的对象,而对象的创建是需要较大代价的,频繁的创建字符串对象,很大程度上会影响系统能。

字符串常量池的使用规则

  • 字符串的创建:

String有两种创建方法,一种是直接赋值,例如String str = “hello”,另一种是通过new创建,String str = newString(“hello”),这两种方法存在较大的差异。

在使用String str = "hello"时,首先会检查字符串常量池中是否存在,若存在,则直接返回常量池中的引用,若不存在,则会在字符串常量池中创建“hello”对象,并返回该对象的引用。

在使用new创建对象时,同样会检查字符串常量池的情况,若运行时常量池中存在“hello”,则直接在堆区中创建一个“hello”对象,若不存在,则会在字符串常量池中先创建一个“hello”,然后再在堆区中创建一个“hello”对象

  • intern方法:

intern是String中的一个native方法,其函数声明如下,会返回一个String对象,例如以下代码:

String str1 = new String("hello");
String str2 = str1.intern();

调用intern方法,会在字符串常量池中查找是否存在相同的字符串,例如这里的“hello”,若存在,则直接返回常量池中该字符串的引用,若不存在,则先在常量池中生成一个字符串对象,再返回该常量的引用

  • ==与equals()

在C++中我们习惯直接使用==对字符串进行比较,然而这在Java中是不可取的,因为==比较的并不是字符串中的内容,而是比较两个字符串是否是同一对象,或者说他们引用的地址是否相同

例如下面这段代码输出结果为false、true。str1与str2是在堆区创建的两个不同的对象,因此比较的结果是false,而str3和str4是对字符串常量池中的同一个常量对象的引用,因此比较的结果是true

    public static void main(String[] args) {
        String str1 = new String("hello");
        String str2 = new String("hello");
        String str3 = "hello";
        String str4 = "hello";
        System.out.println(str1==str2);
        System.out.println(str3==str4);
    }

那么如果想要比较字符串的值是否相同,就可以通过equals方法进行比较,例如以下代码运行结果是true、true、true

public static void main(String[] args) {
    String str1 = new String("hello");
    String str2 = new String("hello");
    String str3 = "hello";
    String str4 = "hello";
    System.out.println(str1.equals(str2));
    System.out.println(str1.equals(str3));
    System.out.println(str3.equals(str4));
}
  • 字符串的拼接:

String对象由于其不可变性,在进行拼接操作时会产生新的对象,但进行拼接的源字符串来自于常量池还是堆区也会对生成的String的位置产生影响。我们使用以下代码进行测试

public static void main(String[] args) {
    String str1 = "hello";
    String str2 = "hel" + "lo";
    String str3 = new String("hel") + "lo";
    String str4 = "hel" + new String("lo");
    String str5 = new String("hel") + new String("lo");
    System.out.println(str1 == str2);
    System.out.println(str1 == str3);
    System.out.println(str1 == str4);
    System.out.println(str1 == str5);
}

输出:

在这里插入图片描述

以上测试证明,两个字符串在进行拼接时,值两个源字符串均为字符串常量时,拼接后产生的字符串才会存放在常量池中,否则将会存放在堆中。

实际上使用“+”进行字符串拼接,会调用StringBuilder的append方法,str=“1”+“2”这样一句话,实际上进行了new StringBuilder().append(“1”).append(“2”)的操作,因而,String使用“+”拼接的效率要略低于StringBuilder.append()方法,append会在下面StringBuffer整理

StringBuffer

StringBuffer与String最大的区别就在于它的可变性,StringBuffer继承自AbstractStringBuilder,是final的类型,大由于它用于存储字符串的char数组并不是final修饰的,因此它是可变的

StringBuffer的创建

StringBuffer有四个构造函数,差别如下:

public StringBuffer();//默认构造函数,默认构造一个长度为16的字符串缓冲区
public StringBuffer(int capacity);//构造一个长度为capacity的字符串缓冲区
public StringBuffer(String str);//构造一个值为str的字符串,并且字符串缓冲区的容量为16+str.length()
public StringBuffer(CharSequence seq);//CharSequence是一个接口,String、StringBuffer与StringBuilder均实现了该接口,因此该构造方法会构造一个值为seq的字符串,并且长度为16+seq.length()

字符串拼接

既然StringBuffer是可变对象,那么自然可以进行字符串拼接并且不用额外生成新的对象,使用append()方法即可,append方法的声明如下,当然实际上在源码中append方法有多个重载,这里没有详细列出来,想详细了解可以直接查看源码

public synchronized StringBuffer append(String str)

append方法的实例如下:

public static void main(String[] args) {
    StringBuffer str = new StringBuffer("Hello ");
    str.append("World!");
    System.out.println(str);
}

StringBuffer的append方法实际上调用了AbstractStringBuilder的append方法,源码如下,其中需要注意的是,、方法最开始会进行判空,若str为null,则会转变为“null”进行拼接,而非“”。

public AbstractStringBuilder append(String str) {
    if (str == null) str = "null";
    int len = str.length();
    if (len == 0) return this;
    int newCount = count + len;
    if (newCount > value.length)
        expandCapacity(newCount);
    str.getChars(0, len, value, count);
    count = newCount;
    return this;
}

其他一些方法

StringBuffer的大多数方法都被synchronized修饰,因此它是线程安全的

public synchronized StringBuffer deleteCharAt(int index);//删除指定位置的字符
public synchronized StringBuffer replace(int start, int end, String str);//替换指定区间的字符串
public synchronized void setCharAt(int index, char ch);//将指定位置的字符设置为ch
public synchronized StringBuffer reverse();//翻转字符串
... ...

StringBuilder

StringBuilder与StringBuffer十分相似,他们都继承自AbstractStringBuilder。String、StringBuffer与StringBuilder的关系如下

在这里插入图片描述

StringBuilder的创建

StringBuilder同样是一个final类,但内部维护了一个非final的char数组,因此他也是可变的,StringBuilder的构造方法与StringBuffer基本一致

public StringBuilder();//默认构造函数,默认构造一个长度为16的字符串缓冲区
public StringBuilder(int capacity);//构造一个长度为capacity的字符串缓冲区
public StringBuilder(String str);//构造一个值为str的字符串,并且字符串缓冲区的容量为16+str.length()
public StringBuilder(CharSequence seq);//CharSequence是一个接口,String、StringBuffer与StringBuilder均实现了该接口,因此该构造方法会构造一个值为seq的字符串,并且长度为16+seq.length()

StringBuilder.append()的一些问题

StringBuilder也有多个append方法的重载,其特点也和StringBuffer基本一致,当传入的对象是null时,会转换为“null”而非“”

StringBuilder.append()在进行拼接时,若拼接后的字符串长度大于StringBuilder的容量capacity,则会调用expandCapacity方法,该方法会尝试开辟一个长度为capacity2+2的新数组,若capacity2+2依然小于新字符串的长度,则会开辟一个长度为新字符串长度的数组,并将原数组复制到新数组中,因而会消耗一定的内存并增加垃圾收集的压力,因此给定一个合适的初始长度可以避免因字符串拼接而大量消耗内存的问题。StringBuffer也存在以上问题

StringBuffer与StringBuilder的差别

StringBuilder的方法都是不加锁的,因此它不是线程安全的,但在单线程的场景下,它的性能要高于StringBuffer,所以多数情况下,使用StringBuilder更为合适,而在涉及并发与多线程的场景下则必须使用StringBuffer

总结

  • String是不可变类,而StringBuffer和StringBuilder均属于可变类
  • String对象可能存在于字符串常量池,也可能存在于堆,而StringBuilder和StringBuffer则存在于堆中
  • String使用“+”进行字符串拼接实际上等同于new StringBuilder.append().append()
  • String由于其不可变性致使其可以在多线程中被安全的共享,而StringBuffer的方法大多都加锁,因而是线程安全的,StringBuilder则是线程不安全的
  • StringBuilder在单线程场景下性能高于StringBuffer
StringStringBufferStringBuilder都是Java中用于处理字符串的类。 String是一个不可变的字符串类,也就是说一旦创建了一个String对象,它的值就不能被修改。每次对String进行修改操作,都会创建一个新的String对象,这样会浪费内存空间和间。因此,当需要频繁地对字符串进行修改使用String并不高效。 StringBufferStringBuilder是可变的字符串类,它们可以被用来进行字符串的修改操作。StringBufferStringBuilder的主要区别在于StringBuffer是线程安全的,而StringBuilder是非线程安全的。这意味着在多线程环境下,如果有多个线程同访问一个StringBuffer对象,它们是安全的;而多个线程同访问一个StringBuilder对象,可能会导致数据错乱。 使用StringBufferStringBuilder的场景通常是在需要频繁地对字符串进行修改的情况下。例如,在循环中拼接字符串、在递归函数中修改字符串等情况下,使用StringBufferStringBuilder可以提高性能。 如果需要将StringBufferStringBuilder转换为String对象,可以使用两种方式。一种是调用它们的toString()方法,将其转换为String对象。另一种是使用String的构造器String(StringBuffer buffer)来创建一个新的String对象,将StringBufferStringBuilder的内容复制到新的String对象中。 总结起来,String是不可变的字符串类,而StringBufferStringBuilder是可变的字符串类,适用于需要频繁修改字符串的场景。转换为String对象可以通过调用toString()方法或使用String的构造器来实现。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值